<template> <focus-trap v-model="consumModeTrap"> <div> <div :class="{ 'cw-structural-element-consumemode': consumeMode }" class="cw-structural-element" v-if="validContext" > <div class="cw-structural-element-content" v-if="structuralElement"> <courseware-ribbon :canEdit="canEdit && canAddElements" :isContentBar="true" @blockAdded="updateContainerList"> <template #buttons> <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id"> <div class="cw-ribbon-button cw-ribbon-button-prev" :title="textRibbon.perv" /> </router-link> <div v-else class="cw-ribbon-button cw-ribbon-button-prev-disabled" :title="$gettext('Keine vorherige Seite')"/> <router-link v-if="nextElement" :to="'/structural_element/' + nextElement.id"> <div class="cw-ribbon-button cw-ribbon-button-next" :title="textRibbon.next" /> </router-link> <div v-else class="cw-ribbon-button cw-ribbon-button-next-disabled" :title="$gettext('Keine nächste Seite')"/> </template> <template #breadcrumbList> <li v-for="ancestor in ancestors" :key="ancestor.id" :title="ancestor.attributes.title" class="cw-ribbon-breadcrumb-item" > <span> <router-link :to="'/structural_element/' + ancestor.id">{{ ancestor.attributes.title || "–" }}</router-link> </span> </li> <li class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current" :title="structuralElement.attributes.title" > <span>{{ structuralElement.attributes.title || "–" }}</span> <span v-if="isTask">[ {{ solverName }} ]</span> </li> </template> <template #breadcrumbFallback> <li class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current" :title="structuralElement.attributes.title" > <span>{{ structuralElement.attributes.title }}</span> </li> </template> <template #menu> <studip-action-menu v-if="!consumeMode" :items="menuItems" class="cw-ribbon-action-menu" :context="structuralElement.attributes.title" @editCurrentElement="menuAction('editCurrentElement')" @addElement="menuAction('addElement')" @deleteCurrentElement="menuAction('deleteCurrentElement')" @showInfo="menuAction('showInfo')" @showExportOptions="menuAction('showExportOptions')" @oerCurrentElement="menuAction('oerCurrentElement')" @setBookmark="menuAction('setBookmark')" @sortContainers="menuAction('sortContainers')" @pdfExport="menuAction('pdfExport')" @showSuggest="menuAction('showSuggest')" @linkElement="menuAction('linkElement')" @removeLock="menuAction('removeLock')" @activateFullscreen="menuAction('activateFullscreen')" /> </template> </courseware-ribbon> <div v-if="structuralElementLoaded && !isLink" class="cw-companion-box-wrapper"> <courseware-companion-box v-if="!canVisit" mood="sad" :msgCompanion="$gettext('Diese Seite steht Ihnen leider nicht zur Verfügung.')" /> <courseware-companion-box v-if="blockedByAnotherUser" :msgCompanion="$gettextInterpolate($gettext('Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet'), {blockingUserName: blockingUserName})" mood="pointing" > <template #companionActions> <button v-if="userIsTeacher" class="button" @click="menuAction('removeLock')"> {{ textRemoveLock.title }} </button> </template> </courseware-companion-box> <courseware-empty-element-box v-if="showEmptyElementBox" :canEdit="canEdit" :noContainers="noContainers" /> <courseware-welcome-screen v-if="noContainers && isRoot && canEdit" /> </div> <div v-if="canVisit && !editView && !isLink" class="cw-container-wrapper" :class="{ 'cw-container-wrapper-consume': consumeMode, 'cw-container-wrapper-discuss': discussView, }" > <courseware-structural-element-discussion v-if="!noContainers && discussView" :structuralElement="structuralElement" :canEdit="canEdit" /> <component v-for="container in containers" :key="container.id" :is="containerComponent(container)" :container="container" :canEdit="canEdit" :canAddElements="canAddElements" :isTeacher="userIsTeacher" class="cw-container-item" /> </div> <div v-if="isLink" class="cw-container-wrapper" :class="{ 'cw-container-wrapper-consume': consumeMode, 'cw-container-wrapper-discuss': discussView, }" > <courseware-structural-element-discussion v-if="discussView" :structuralElement="structuralElement" :canEdit="canEdit" /> <div v-if="editView" class="cw-companion-box-wrapper"> <courseware-companion-box :msgCompanion="$gettextInterpolate($gettext('Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.'), { ownerName: ownerName })" mood="pointing" /> </div> <component v-for="container in linkedContainers" :key="container.id" :is="containerComponent(container)" :container="container" :canEdit="false" :canAddElements="false" :isTeacher="userIsTeacher" class="cw-container-item" /> </div> <div v-if="canVisit && canEdit && editView && !isLink" 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"> {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}} </span> <draggable class="cw-structural-element-list" tag="ol" role="listbox" v-model="containerList" v-bind="dragOptions" handle=".cw-sortable-handle" @start="isDragging = true" @end="dropContainer" > <li v-for="container in containerList" :key="container.id" class="cw-container-item-sortable" > <span :class="{ 'cw-sortable-handle-dragging': isDragging }" class="cw-sortable-handle" tabindex="0" role="option" aria-describedby="operation" :ref="'sortableHandle' + container.id" @keydown="keyHandler($event, container.id)" ></span> <component :is="containerComponent(container)" :container="container" :canEdit="canEdit" :canAddElements="canAddElements" :isTeacher="userIsTeacher" class="cw-container-item" ref="containers" :class="{ 'cw-container-item-selected': keyboardSelected === container.id}" /> </li> </draggable> </template> <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" /> </div> </div> <studip-dialog v-if="showEditDialog" :title="textEdit.title" :confirmText="textEdit.confirm" confirmClass="accept" :closeText="textEdit.close" closeClass="cancel" height="500" :width="inContent ? '720' : '500'" class="studip-dialog-with-tab" @close="closeEditDialog" @confirm="storeCurrentElement" > <template v-slot:dialogContent> <courseware-tabs class="cw-tab-in-dialog"> <courseware-tab :name="textEdit.basic" :selected="true" :index="0"> <form class="default" @submit.prevent=""> <label> <translate>Titel</translate> <input type="text" v-model="currentElement.attributes.title" /> </label> <label> <translate>Beschreibung</translate> <textarea v-model="currentElement.attributes.payload.description" class="cw-structural-element-description" /> </label> </form> </courseware-tab> <courseware-tab :name="textEdit.meta" :index="1"> <form class="default" @submit.prevent=""> <label> <translate>Farbe</translate> <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> <translate>Es steht keine Auswahl zur Verfügung</translate>. </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> <translate>Art des Lernmaterials</translate> <select v-model="currentElement.attributes.purpose"> <option value="content"><translate>Inhalt</translate></option> <option v-if="!inCourse" value="template"><translate>Aufgabenvorlage</translate></option> <option value="oer"><translate>OER-Material</translate></option> <option value="portfolio"><translate>ePortfolio</translate></option> <option value="draft"><translate>Entwurf</translate></option> <option value="other"><translate>Sonstiges</translate></option> </select> </label> <label> <translate>Lizenztyp</translate> <select v-model="currentElement.attributes.payload.license_type"> <option v-for="license in licenses" :key="license.id" :value="license.id"> {{ license.name }} </option> </select> </label> <label> <translate>Geschätzter zeitlicher Aufwand</translate> <input type="text" v-model="currentElement.attributes.payload.required_time" /> </label> <label> <translate>Niveau</translate><br /> <translate>von</translate> <select v-model="currentElement.attributes.payload.difficulty_start"> <option v-for="difficulty_start in 12" :key="difficulty_start" :value="difficulty_start" > {{ difficulty_start }} </option> </select> <translate>bis</translate> <select v-model="currentElement.attributes.payload.difficulty_end"> <option v-for="difficulty_end in 12" :key="difficulty_end" :value="difficulty_end" > {{ difficulty_end }} </option> </select> </label> </form> </courseware-tab> <courseware-tab :name="textEdit.image" :index="2"> <form class="default" @submit.prevent=""> <img v-if="showPreviewImage" :src="image" class="cw-structural-element-image-preview" :alt="$gettext('Vorschaubild')" /> <label v-if="showPreviewImage"> <button class="button" @click="deleteImage" v-translate>Bild löschen</button> </label> <div v-if="uploadFileError" class="messagebox messagebox_error"> {{ uploadFileError }} </div> <label v-if="!showPreviewImage"> <translate>Bild hochladen</translate> <input ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" /> </label> </form> </courseware-tab> <courseware-tab v-if="(inCourse && !isTask) || inContent" :name="textEdit.approval" :index="3"> <courseware-structural-element-permissions v-if="inCourse" :element="currentElement" @updateReadApproval="updateReadApproval" @updateWriteApproval="updateWriteApproval" /> <courseware-content-permissions v-if="inContent" :element="currentElement" @updateReadApproval="updateReadApproval" @updateWriteApproval="updateWriteApproval" /> </courseware-tab> <courseware-tab v-if="inCourse && !isTask" :name="textEdit.visible" :index="4"> <form class="default" @submit.prevent=""> <label> <translate>Sichtbar ab</translate> <input type="date" v-model="currentElement.attributes['release-date']" /> </label> <label> <translate>Unsichtbar ab</translate> <input type="date" v-model="currentElement.attributes['withdraw-date']" /> </label> </form> </courseware-tab> </courseware-tabs> </template> </studip-dialog> <studip-dialog v-if="showAddDialog" :title="$gettext('Seite hinzufügen')" :confirmText="$gettext('Erstellen')" confirmClass="accept" :closeText="$gettext('Schließen')" closeClass="cancel" class="cw-structural-element-dialog" :height="inCourse ? '300' : '430'" @close="closeAddDialog" @confirm="createElement" > <template v-slot:dialogContent> <form class="default" @submit.prevent=""> <label> <translate>Position der neuen Seite</translate> <select v-model="newChapterParent"> <option v-if="!isRoot && canEditParent" value="sibling"> <translate>Neben der aktuellen Seite</translate> </option> <option value="descendant"><translate>Unterhalb der aktuellen Seite</translate></option> </select> </label> <label> <translate>Name der neuen Seite</translate><br /> <input v-model="newChapterName" type="text" /> </label> <label v-if="!inCourse"> <translate>Art des Lernmaterials</translate> <select v-model="newChapterPurpose"> <option value="content"><translate>Inhalt</translate></option> <option v-if="!inCourse" value="template"><translate>Aufgabenvorlage</translate></option> <option value="oer"><translate>OER-Material</translate></option> <option value="portfolio"><translate>ePortfolio</translate></option> <option value="draft"><translate>Entwurf</translate></option> <option value="other"><translate>Sonstiges</translate></option> </select> </label> <label v-if="!inCourse"> <translate>Lernmaterialvorlage</translate> <select v-model="newChapterTemplate"> <option :value="null"><translate>ohne Vorlage</translate></option> <option v-for="template in selectableTemplates" :key="template.id" :value="template" > {{ template.attributes.name }} </option> </select> </label> </form> </template> </studip-dialog> <studip-dialog v-if="showInfoDialog" :title="textInfo.title" :closeText="textInfo.close" closeClass="cancel" @close="showElementInfoDialog(false)" > <template v-slot:dialogContent> <table class="cw-structural-element-info"> <tr> <td><translate>Titel</translate>:</td> <td>{{ structuralElement.attributes.title }}</td> </tr> <tr> <td><translate>Beschreibung</translate>:</td> <td>{{ structuralElement.attributes.payload.description }}</td> </tr> <tr> <td><translate>Seite wurde erstellt von</translate>:</td> <td>{{ ownerName }}</td> </tr> <tr> <td><translate>Seite wurde erstellt am</translate>:</td> <td><iso-date :date="structuralElement.attributes.mkdate" /></td> </tr> <tr> <td><translate>Zuletzt bearbeitet von</translate>:</td> <td>{{ editorName }}</td> </tr> <tr> <td><translate>Zuletzt bearbeitet am</translate>:</td> <td><iso-date :date="structuralElement.attributes.chdate" /></td> </tr> </table> </template> </studip-dialog> <studip-dialog v-if="showExportDialog" :title="textExport.title" :confirmText="textExport.confirm" confirmClass="accept" :closeText="textExport.close" closeClass="cancel" height="350" @close="showElementExportDialog(false)" @confirm="exportCurrentElement" > <template v-slot:dialogContent> <div v-show="!exportRunning"> <span v-translate>Hiermit exportieren Sie die Seite "%{ currentElement.attributes.title }" als ZIP-Datei.</span> <div class="cw-element-export"> <label> <input type="checkbox" v-model="exportChildren" /> <translate>Unterseiten exportieren</translate> </label> </div> </div> <courseware-companion-box v-show="exportRunning" :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')" mood="pointing" /> <div v-show="exportRunning" class="cw-import-zip"> <header>{{ exportState }}:</header> <div class="progress-bar-wrapper"> <div class="progress-bar" role="progressbar" :style="{ width: exportProgress + '%' }" :aria-valuenow="exportProgress" aria-valuemin="0" aria-valuemax="100" > {{ exportProgress }}% </div> </div> </div> </template> </studip-dialog> <studip-dialog v-if="showPdfExportDialog" :title="textExport.title" :confirmText="textExport.confirm" confirmClass="accept" :closeText="textExport.close" closeClass="cancel" height="350" @close="showElementPdfExportDialog(false)" @confirm="pdfExportCurrentElement" > <template v-slot:dialogContent> <span v-translate>Hiermit exportieren Sie die Seite "%{ currentElement.attributes.title }" als PDF-Datei.</span> <div class="cw-element-export"> <label> <input type="checkbox" v-model="pdfExportChildren" /> <translate>Unterseiten exportieren</translate> </label> </div> </template> </studip-dialog> <studip-dialog v-if="showOerDialog" height="600" width="600" :title="textOer.title" :confirmText="textOer.confirm" confirmClass="accept" :closeText="textOer.close" closeClass="cancel" @close="showElementOerDialog(false)" @confirm="publishCurrentElement" > <template v-slot:dialogContent> <form v-show="!oerExportRunning" class="default" @submit.prevent=""> <fieldset> <legend><translate>Grunddaten</translate></legend> <label> <p><translate>Vorschaubild</translate>:</p> <img v-if="currentElement.relationships.image.data" :src="currentElement.relationships.image.meta['download-url']" width="400" /> </label> <label> <p><translate>Beschreibung</translate>:</p> <p>{{ currentElement.attributes.payload.description }}</p> </label> <label> <translate>Niveau</translate>: <p> {{ currentElement.attributes.payload.difficulty_start }} - {{ currentElement.attributes.payload.difficulty_end }} </p> </label> <label> <translate>Lizenztyp</translate>: <p>{{ currentLicenseName }}</p> </label> <label> <translate>Sie können diese Daten unter "Seite bearbeiten" verändern.</translate> </label> </fieldset> <fieldset> <legend><translate>Einstellungen</translate></legend> <label> <translate>Unterseiten veröffentlichen</translate> <input type="checkbox" v-model="oerChildren" /> </label> </fieldset> </form> <courseware-companion-box v-show="oerExportRunning" :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')" mood="pointing" /> </template> </studip-dialog> <studip-dialog v-if="showSuggestOerDialog" height="600" width="600" :title="textSuggestOer.title" :confirmText="textSuggestOer.confirm" confirmClass="accept" :closeText="textSuggestOer.close" closeClass="cancel" @close="updateShowSuggestOerDialog(false)" @confirm="sendOerSuggestion" > <template v-slot:dialogContent> <p v-translate>Das folgende Courseware-Material wird %{ ownerName } zur Veröffentlichung im OER Campus vorgeschlagen:</p> <table class="cw-structural-element-info"> <tr> <td><translate>Titel</translate>:</td> <td>{{ structuralElement.attributes.title }}</td> </tr> <tr> <td><translate>Beschreibung</translate>:</td> <td>{{ structuralElement.attributes.payload.description }}</td> </tr> </table> <form class="default" @submit.prevent=""> <label> <translate>Ihr Vorschlag wird anonym versendet. Falls gewünscht, können Sie zusätzlich eine Nachricht verfassen:</translate> <textarea v-model="additionalText" class="cw-structural-element-description" /> </label> </form> </template> </studip-dialog> <studip-dialog v-if="showDeleteDialog" :title="textDelete.title" :question="textDelete.alert" height="200" @confirm="deleteCurrentElement" @close="closeDeleteDialog" ></studip-dialog> <studip-dialog v-if="showPublicLinkDialog && inContent" :title="$gettext('Öffentlichen Link für Seite erzeugen')" :confirmText="$gettext('Erstellen')" confirmClass="accept" :closeText="$gettext('Abbrechen')" closeClass="cancel" class="cw-structural-element-dialog" @close="closePublicLinkDialog" @confirm="createElementPublicLink" > <template v-slot:dialogContent> <form class="default" @submit.prevent=""> <label> <translate>Passwort</translate> <input type="password" v-model="publicLink.password" /> </label> <label> <translate>Ablaufdatum</translate> <input v-model="publicLink['expire-date']" type="date" class="size-l" /> </label> </form> </template> </studip-dialog> <studip-dialog v-if="showRemoveLockDialog" :title="textRemoveLock.title" :question="textRemoveLock.alert" height="200" width="450" @confirm="executeRemoveLock" @close="showElementRemoveLockDialog(false)" ></studip-dialog> <courseware-structural-element-dialog-import v-if="showImportDialog"/> <courseware-structural-element-dialog-copy v-if="showCopyDialog" /> <courseware-structural-element-dialog-link v-if="showLinkDialog"/> </div> <div v-else> <courseware-companion-box v-if="currentElement !== ''" :msgCompanion="textCompanionWrongContext" mood="sad" /> </div> </div> </focus-trap> </template> <script> import ContainerComponents from './container-components.js'; import CoursewarePluginComponents from './plugin-components.js'; import CoursewareStructuralElementDialogCopy from './CoursewareStructuralElementDialogCopy.vue'; import CoursewareStructuralElementDialogImport from './CoursewareStructuralElementDialogImport.vue'; import CoursewareStructuralElementDialogLink from './CoursewareStructuralElementDialogLink.vue'; import CoursewareStructuralElementDiscussion from './CoursewareStructuralElementDiscussion.vue'; import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue'; import CoursewareContentPermissions from './CoursewareContentPermissions.vue'; import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue'; import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; import CoursewareWelcomeScreen from './CoursewareWelcomeScreen.vue'; import CoursewareEmptyElementBox from './CoursewareEmptyElementBox.vue'; import CoursewareListContainer from './CoursewareListContainer.vue'; import CoursewareTabsContainer from './CoursewareTabsContainer.vue'; import CoursewareRibbon from './CoursewareRibbon.vue'; import CoursewareTabs from './CoursewareTabs.vue'; import CoursewareTab from './CoursewareTab.vue'; import CoursewareExport from '@/vue/mixins/courseware/export.js'; import CoursewareOerMessage from '@/vue/mixins/courseware/oermessage.js'; import colorMixin from '@/vue/mixins/courseware/colors.js'; import CoursewareDateInput from './CoursewareDateInput.vue'; import { FocusTrap } from 'focus-trap-vue'; import IsoDate from './IsoDate.vue'; import StudipDialog from '../StudipDialog.vue'; import draggable from 'vuedraggable'; import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-structural-element', components: { CoursewareStructuralElementDialogCopy, CoursewareStructuralElementDialogImport, CoursewareStructuralElementDialogLink, CoursewareStructuralElementDiscussion, CoursewareStructuralElementPermissions, CoursewareContentPermissions, CoursewareRibbon, CoursewareListContainer, CoursewareAccordionContainer, CoursewareTabsContainer, CoursewareCompanionBox, CoursewareWelcomeScreen, CoursewareEmptyElementBox, CoursewareTabs, CoursewareTab, CoursewareDateInput, FocusTrap, IsoDate, StudipDialog, draggable, }, props: ['canVisit', 'orderedStructuralElements', 'structuralElement'], mixins: [CoursewareExport, CoursewareOerMessage, colorMixin], data() { return { newChapterName: '', newChapterParent: 'descendant', newChapterPurpose: 'content', newChapterTemplate: null, currentElement: '', uploadFileError: '', textCompanionWrongContext: this.$gettext('Die angeforderte Seite ist nicht Teil dieser Courseware.'), textEdit: { title: this.$gettext('Seite bearbeiten'), confirm: this.$gettext('Speichern'), close: this.$gettext('Schließen'), basic: this.$gettext('Grunddaten'), image: this.$gettext('Bild'), meta: this.$gettext('Metadaten'), approval: this.$gettext('Rechte'), visible: this.$gettext('Sichtbarkeit'), }, textInfo: { title: this.$gettext('Informationen zur Seite'), close: this.$gettext('Schließen'), }, textExport: { title: this.$gettext('Seite exportieren'), confirm: this.$gettext('Exportieren'), close: this.$gettext('Schließen'), }, textAdd: { title: this.$gettext('Seite hinzufügen'), confirm: this.$gettext('Erstellen'), close: this.$gettext('Schließen'), }, textRibbon: { perv: this.$gettext('zurück'), next: this.$gettext('weiter'), }, textRemoveLock: { title: this.$gettext('Sperre aufheben'), alert: this.$gettext('Möchten Sie die Sperre der Seite wirklich aufheben?'), }, exportRunning: false, exportChildren: false, oerExportRunning: false, oerChildren: true, pdfExportChildren: false, containerList: [], isDragging: false, dragOptions: { animation: 0, group: 'description', disabled: false, ghostClass: 'container-ghost', }, errorEmptyChapterName: false, consumModeTrap: false, additionalText: '', publicLink: { passsword: '', 'expire-date': '' }, deletingPreviewImage: false, processing: false, keyboardSelected: null, assistiveLive: '' }; }, computed: { ...mapGetters({ courseware: 'courseware', context: 'context', consumeMode: 'consumeMode', containerById: 'courseware-containers/byId', relatedContainers: 'courseware-containers/related', relatedStructuralElements: 'courseware-structural-elements/related', relatedTaskGroups: 'courseware-task-groups/related', relatedUsers: 'users/related', structuralElementById: 'courseware-structural-elements/byId', userIsTeacher: 'userIsTeacher', pluginManager: 'pluginManager', showEditDialog: 'showStructuralElementEditDialog', showAddDialog: 'showStructuralElementAddDialog', showImportDialog: 'showStructuralElementImportDialog', showCopyDialog: 'showStructuralElementCopyDialog', showLinkDialog: 'showStructuralElementLinkDialog', showExportDialog: 'showStructuralElementExportDialog', showPdfExportDialog: 'showStructuralElementPdfExportDialog', showInfoDialog: 'showStructuralElementInfoDialog', showDeleteDialog: 'showStructuralElementDeleteDialog', showOerDialog: 'showStructuralElementOerDialog', showSuggestOerDialog: 'showSuggestOerDialog', showPublicLinkDialog: 'showStructuralElementPublicLinkDialog', showRemoveLockDialog: 'showStructuralElementRemoveLockDialog', oerEnabled: 'oerEnabled', licenses: 'licenses', exportState: 'exportState', exportProgress: 'exportProgress', userId: 'userId', viewMode: 'viewMode', taskById: 'courseware-tasks/byId', userById: 'users/byId', lastCreatedElement: 'courseware-structural-elements/lastCreated', groupById: 'status-groups/byId', blocked: 'currentElementBlocked', blockerId: 'currentElementBlockerId', blockedByThisUser: 'currentElementBlockedByThisUser', blockedByAnotherUser: 'currentElementBlockedByAnotherUser', isLink: 'currentElementisLink', templates: 'courseware-templates/all', }), currentId() { return this.structuralElement?.id; }, textOer() { return { title: this.$gettext('Lerninhalte auf dem OER Campus veröffentlichen'), confirm: this.$gettext('Veröffentlichen'), close: this.$gettext('Abbrechen'), }; }, textSuggestOer() { return { title: this.$gettext('Lerninhalt für den OER Campus vorschlagen'), confirm: this.$gettext('Lerninhalt vorschlagen'), close: this.$gettext('Abbrechen'), }; }, inCourse() { return this.context.type === 'courses'; }, inContent() { // The rights tab in contents will be only visible to the owner. return this.context.type === 'users' && this.userId === this.currentElement.relationships.user.data.id; }, textDelete() { let textDelete = {}; textDelete.title = this.$gettext('Seite unwiderruflich löschen'); textDelete.alert = this.$gettext('Möchten Sie die Seite wirklich löschen?'); if (this.structuralElementLoaded) { textDelete.alert = this.$gettextInterpolate( this.$gettext('Möchten Sie die Seite %{ pageTitle } und alle ihre Unterseiten wirklich löschen?'), {pageTitle: this.structuralElement.attributes.title} ); } return textDelete; }, validContext() { let valid = false; if (this.context.type === 'courses' && this.currentElement.relationships) { if ( this.currentElement.relationships.course && this.context.id === this.currentElement.relationships.course.data.id ) { valid = true; } } if (this.context.type === 'users' && this.currentElement.relationships) { if ( this.currentElement.relationships.user && this.context.id === this.currentElement.relationships.user.data.id ) { valid = true; } } if (this.context.type === 'sharedusers') { if (this.context.id === this.courseware.relationships.root.data.id) { valid = true; } } if (this.context.type === 'public') { valid = true; } return valid; }, image() { return this.structuralElement.relationships?.image?.meta?.['download-url'] ?? null; }, showPreviewImage() { return this.image !== null && this.deletingPreviewImage === false; }, structuralElementLoaded() { return this.structuralElement !== null && this.structuralElement !== {}; }, ancestors() { if (!this.structuralElement) { return []; } const finder = (parent) => { const parentId = parent.relationships?.parent?.data?.id; if (!parentId) { return null; } const element = this.structuralElementById({ id: parentId }); if (!element) { console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`); } return element; }; const visitAncestors = function* (node) { const parent = finder(node); if (parent) { yield parent; yield* visitAncestors(parent); } }; return [...visitAncestors(this.structuralElement)].reverse(); }, prevElement() { const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id); if (currentIndex <= 0) { return null; } const previousId = this.orderedStructuralElements[currentIndex - 1]; const previous = this.structuralElementById({ id: previousId }); return previous; }, nextElement() { const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id); const lastIndex = this.orderedStructuralElements.length - 1; if (currentIndex === -1 || currentIndex === lastIndex) { return null; } const nextId = this.orderedStructuralElements[currentIndex + 1]; const next = this.structuralElementById({ id: nextId }); return next; }, empty() { if (this.containers === null) { return true; } else { return !this.containers.some((container) => container.relationships.blocks.data.length > 0); } }, containers() { if (!this.structuralElement) { return []; } return ( this.relatedContainers({ parent: this.structuralElement, relationship: 'containers', }) ?? [] ); }, noContainers() { if (this.containers === null) { return true; } else { return this.containers.length === 0; } }, canEdit() { if (!this.structuralElement) { return false; } return this.structuralElement.attributes['can-edit']; }, canEditParent() { if (this.isRoot) { return false; } const parentId = this.structuralElement.relationships.parent.data.id; const parent = this.structuralElementById({ id: parentId }); return parent.attributes['can-edit']; }, isRoot() { return this.structuralElement.relationships.parent.data === null; }, editor() { const editor = this.relatedUsers({ parent: this.structuralElement, relationship: 'editor', }); return editor ?? null; }, editorName() { return this.editor?.attributes['formatted-name'] ?? '?'; }, menuItems() { let menu = [ { id: 4, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' }, { id: 5, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' }, { id: 6, label: this.$gettext('Lerninhalt für OER Campus vorschlagen'), icon: 'oer-campus', emit: 'showSuggest' } ]; if (!document.documentElement.classList.contains('responsive-display')) { menu.push( { id: 7, label: this.$gettext('Als Vollbild anzeigen'), icon: 'screen-full', emit: 'activateFullscreen'}, ); } if (this.canEdit) { if (!this.blockedByAnotherUser) { menu.push({ id: 1, label: this.$gettext('Seite bearbeiten'), icon: 'edit', emit: 'editCurrentElement', }); } if (this.blockedByAnotherUser && this.userIsTeacher) { menu.push({ id: 1, label: this.textRemoveLock.title, icon: 'lock-unlocked', emit: 'removeLock', }); } menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' }); } 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) { menu.push({ id: 8, label: this.$gettext('Seite löschen'), icon: 'trash', emit: 'deleteCurrentElement', }); } menu.sort((a, b) => a.id - b.id); return menu; }, colors() { return this.mixinColors.filter(color => color.darkmode); }, currentLicenseName() { for (let i = 0; i < this.licenses.length; i++) { if (this.licenses[i]['id'] == this.currentElement.attributes.payload.license_type) { return this.licenses[i]['name']; } } return ''; }, blockingUser() { if (this.blockedByAnotherUser) { return this.userById({id: this.blockerId}); } return null; }, blockingUserName() { return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : ''; }, discussView() { return this.viewMode === 'discuss'; }, editView() { return this.viewMode === 'edit'; }, pdfExportURL() { if (this.context.type === 'users') { return STUDIP.URLHelper.getURL( 'dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id ); } if (this.context.type === 'courses') { return STUDIP.URLHelper.getURL( 'dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id ); } return ''; }, isTask() { return this.structuralElement?.relationships.task.data !== null; }, task() { if (!this.isTask) { return null; } return this.taskById({ id: this.structuralElement.relationships.task.data.id }); }, solver() { if (this.task) { const solver = this.task.relationships.solver.data; if (solver.type === 'users') { return this.userById({ id: solver.id }); } if (solver.type === 'status-groups') { return this.groupById({ id: solver.id }); } } return null; }, solverName() { if (this.solver) { if (this.solver.type === 'users') { return this.solver.attributes['formatted-name']; } if (this.solver.type === 'status-groups') { return this.solver.attributes.name; } } return ''; }, canAddElements() { if (!this.isTask) { return true; } // still loading if (!this.task) { return false; } const taskGroup = this.relatedTaskGroups({ parent: this.task, relationship: 'task-group' }); 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) { return this.structuralElementById({ id: this.structuralElement.attributes['target-id']}); } return null; }, linkedContainers() { let containers = []; let relatedContainers = this.linkedElement?.relationships?.containers?.data; if (relatedContainers) { for (const container of relatedContainers) { containers.push(this.containerById({ id: container.id})); } } return containers; }, owner() { const owner = this.relatedUsers({ parent: this.structuralElement, relationship: 'owner', }); return owner ?? null; }, ownerName() { return this.owner?.attributes['formatted-name'] ?? '?'; }, selectableTemplates() { return this.templates.filter(template => { return template.attributes.purpose === this.newChapterPurpose }); }, }, methods: { ...mapActions({ createStructuralElementWithTemplate: 'createStructuralElementWithTemplate', updateStructuralElement: 'updateStructuralElement', deleteStructuralElement: 'deleteStructuralElement', lockObject: 'lockObject', unlockObject: 'unlockObject', addBookmark: 'addBookmark', companionInfo: 'companionInfo', companionWarning: 'companionWarning', companionError: 'companionError', uploadImageForStructuralElement: 'uploadImageForStructuralElement', deleteImageForStructuralElement: 'deleteImageForStructuralElement', companionSuccess: 'companionSuccess', showElementEditDialog: 'showElementEditDialog', showElementAddDialog: 'showElementAddDialog', showElementExportDialog: 'showElementExportDialog', showElementPdfExportDialog: 'showElementPdfExportDialog', showElementInfoDialog: 'showElementInfoDialog', showElementDeleteDialog: 'showElementDeleteDialog', showElementOerDialog: 'showElementOerDialog', showElementPublicLinkDialog: 'showElementPublicLinkDialog', showElementRemoveLockDialog: 'showElementRemoveLockDialog', updateShowSuggestOerDialog: 'updateShowSuggestOerDialog', updateContainer: 'updateContainer', sortContainersInStructualElements: 'sortContainersInStructualElements', loadTask: 'loadTask', loadStructuralElement: 'loadStructuralElement', createLink: 'createLink', setCurrentElementId: 'coursewareCurrentElement', }), initCurrent() { this.currentElement = _.cloneDeep(this.structuralElement); this.uploadFileError = ''; this.deletingPreviewImage = false; }, async menuAction(action) { switch (action) { case 'removeLock': this.displayRemoveLockDialog(); break; case 'editCurrentElement': await this.loadStructuralElement(this.currentId); if (this.blockedByAnotherUser) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); return false; } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); } catch(error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); } else { console.log(error); } return false; } this.initCurrent(); this.showElementEditDialog(true); break; case 'addElement': this.newChapterName = ''; this.newChapterParent = 'descendant'; this.errorEmptyChapterName = false; this.showElementAddDialog(true); break; case 'deleteCurrentElement': await this.loadStructuralElement(this.currentId); if (this.blockedByAnotherUser) { this.companionInfo({ info: this.$gettextInterpolate( this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Seite bearbeitet.'), {blockingUserName: this.blockingUserName} ) }); return false; } await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); this.showElementDeleteDialog(true); break; case 'showInfo': this.showElementInfoDialog(true); break; case 'showExportOptions': this.showElementExportDialog(true); break; case 'oerCurrentElement': this.showElementOerDialog(true); break; case 'showSuggest': this.updateShowSuggestOerDialog(true); break; case 'setBookmark': this.setBookmark(); break; case 'linkElement': this.showElementPublicLinkDialog(true); break; case 'activateFullscreen': STUDIP.Fullscreen.activate(); break; } }, async closeEditDialog() { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByThisUser) { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); await this.loadStructuralElement(this.currentElement.id); } this.showElementEditDialog(false); this.initCurrent(); }, closeAddDialog() { this.showElementAddDialog(false); }, checkUploadFile() { const file = this.$refs?.upload_image?.files[0]; if (file.size > 2097152) { this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine kleinere Datei.'); } else if (!file.type.includes('image')) { this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.'); } else { this.uploadFileError = ''; } }, deleteImage() { if (!this.deletingPreviewImage) { this.deletingPreviewImage = true; } }, async storeCurrentElement() { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByAnotherUser) { this.companionWarning({ info: this.$gettextInterpolate( this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'), {blockingUserName: this.blockingUserName} ) }); this.showElementEditDialog(false); return false; } if (!this.blocked) { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); } const file = this.$refs?.upload_image?.files[0]; if (file) { if (file.size > 2097152) { return false; } this.uploadFileError = ''; this.uploadImageForStructuralElement({ structuralElement: this.currentElement, file, }).catch((error) => { console.error(error); this.uploadFileError = this.$gettext('Fehler beim Hochladen der Datei.'); }); await this.loadStructuralElement(this.currentElement.id); } else if (this.deletingPreviewImage) { await this.deleteImageForStructuralElement(this.currentElement); } this.showElementEditDialog(false); if (this.currentElement.attributes['release-date'] !== '') { this.currentElement.attributes['release-date'] = new Date(this.currentElement.attributes['release-date']).getTime() / 1000; } if (this.currentElement.attributes['withdraw-date'] !== '') { this.currentElement.attributes['withdraw-date'] = new Date(this.currentElement.attributes['withdraw-date']).getTime() / 1000; } await this.updateStructuralElement({ element: this.currentElement, id: this.currentId, }); await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); this.$emit('select', this.currentId); this.initCurrent(); }, dropContainer() { this.isDragging = false; this.storeSort(); }, async storeSort() { const timeout = setTimeout(() => this.processing = true, 800); if (this.blockedByAnotherUser) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); clearTimeout(timeout); this.processing = false; return false; } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); } catch (error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); } else { console.log(error); } clearTimeout(timeout); this.processing = false; return false; } await this.sortContainersInStructualElements({ structuralElement: this.structuralElement, containers: this.containerList, }); this.$emit('select', this.currentId); clearTimeout(timeout); this.processing = false; }, async exportCurrentElement(data) { if (this.exportRunning) { return; } this.exportRunning = true; await this.sendExportZip(this.currentElement.id, { withChildren: this.exportChildren, }); this.exportRunning = false; this.showElementExportDialog(false); }, pdfExportCurrentElement() { this.showElementPdfExportDialog(false); let url = ''; let withChildren = this.pdfExportChildren ? '/1' : '/0'; if (this.context.type === 'users') { url = STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id + withChildren); } if (this.context.type === 'courses') { url = STUDIP.URLHelper.getURL('dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id + withChildren); } if (url) { window.open(url , '_blank').focus(); } }, async publishCurrentElement() { if (this.oerExportRunning) { return; } this.oerExportRunning = true; await this.exportToOER(this.currentElement, { withChildren: this.oerChildren }); this.oerExportRunning = false; this.showElementOerDialog(false); }, async closeDeleteDialog() { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByThisUser) { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); } this.showElementDeleteDialog(false); }, async deleteCurrentElement() { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByAnotherUser) { this.companionWarning({ info: this.$gettextInterpolate( this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'), {blockingUserName: this.blockingUserName} ) }); this.showElementDeleteDialog(false); return false; } let parent_id = this.structuralElement.relationships.parent.data.id; this.showElementDeleteDialog(false); this.companionInfo({ info: this.$gettext('Lösche Seite und alle darunter liegenden Elemente.') }); this.deleteStructuralElement({ id: this.currentId, parentId: this.structuralElement.relationships.parent.data.id, }) .then(response => { this.$router.push(parent_id); this.companionInfo({ info: this.$gettext('Die Seite wurde gelöscht.') }); }) .catch(error => { this.companionError({ info: this.$gettext('Die Seite konnte nicht gelöscht werden.') }); }); }, async createElement() { const title = this.newChapterName; // this is the title of the new element const purpose = this.newChapterPurpose; let parent_id = this.currentId; // new page is descandant as default this.errorEmptyChapterName = title.trim(); if (this.errorEmptyChapterName === '') { return; } if (this.newChapterParent === 'sibling') { parent_id = this.structuralElement.relationships.parent.data.id; } this.showElementAddDialog(false); this.createStructuralElementWithTemplate({ attributes: { title: title, purpose: purpose, }, templateId: this.newChapterTemplate ? this.newChapterTemplate.id : null, parentId: parent_id, currentId: this.currentId, }) .then(() => { let newElement = this.$store.getters['courseware-structural-elements/lastCreated']; this.companionSuccess({ info: this.$gettextInterpolate( this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), { pageTitle: newElement.attributes.title } ) }); }) .catch(e => { let errorMessage = this.$gettext('Es ist ein Fehler aufgetreten. Die Seite konnte nicht erstellt werden.'); if (e.status === 403) { errorMessage = this.$gettext('Die Seite konnte nicht erstellt werden. Sie haben nicht die notwendigen Schreibrechte.'); } this.companionError({ info: errorMessage }); }); let newElement = this.lastCreatedElement; this.companionSuccess({ info: this.$gettextInterpolate( this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), {pageTitle: newElement.attributes.title} ) }); this.newChapterName = ''; }, containerComponent(container) { return 'courseware-' + container.attributes['container-type'] + '-container'; }, setBookmark() { this.addBookmark(this.structuralElement); this.companionInfo({ info: this.$gettext('Das Lesezeichen wurde gesetzt.') }); }, updateReadApproval(approval) { this.currentElement.attributes['read-approval'] = approval; }, updateWriteApproval(approval) { this.currentElement.attributes['write-approval'] = approval; }, sendOerSuggestion() { this.suggestViaAction(this.currentElement, this.additionalText); this.updateShowSuggestOerDialog(false); }, async createElementPublicLink() { const date = this.publicLink['expire-date']; const publicLink = { attributes: { password: this.publicLink.password, 'expire-date': date === '' ? new Date(0).toISOString() : new Date(date).toISOString() }, relationships: { 'structural-element': { data: { id: this.currentElement.id, type: 'courseware-structural-elements' } } } } await this.createLink({ publicLink }); this.companionSuccess({ info: this.$gettext('Öffentlicher Link wurde angelegt. Unter Freigaben finden Sie alle Ihre öffentlichen Links.'), }); this.closeLinkDialog(); }, closePublicLinkDialog() { this.publicLink = { passsword: '', 'expire-date': '' }; this.showElementPublicLinkDialog(false); }, displayRemoveLockDialog() { this.showElementRemoveLockDialog(true); }, async executeRemoveLock() { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); await this.loadStructuralElement(this.currentElement.id); this.showElementRemoveLockDialog(false); }, updateContainerList() { this.containerList = this.containers; const containerRefs = this.$refs.containers; for (let ref of containerRefs) { ref.initCurrentData(); } }, keyHandler(e, containerId) { switch (e.keyCode) { case 27: // esc this.abortKeyboardSorting(containerId); break; case 32: // space e.preventDefault(); if (this.keyboardSelected) { this.storeKeyboardSorting(containerId); } else { this.keyboardSelected = containerId; const container = this.containerById({id: containerId}); const index = this.containerList.findIndex(c => c.id === container.id); this.assistiveLive = this.$gettextInterpolate( this.$gettext('%{containerTitle} Abschnitt ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.') , {containerTitle: container.attributes.title, pos: index + 1, listLength: this.containerList.length} ); } break; } if (this.keyboardSelected) { switch (e.keyCode) { case 9: //tab this.abortKeyboardSorting(containerId); break; case 38: // up e.preventDefault(); this.moveItemUp(containerId); break; case 40: // down e.preventDefault(); this.moveItemDown(containerId); break; } } }, moveItemUp(containerId) { const currentIndex = this.containerList.findIndex(container => container.id === containerId); if (currentIndex !== 0) { const container = this.containerById({id: containerId}); const newPos = currentIndex - 1; this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]); this.assistiveLive = this.$gettextInterpolate( this.$gettext('%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.') , {containerTitle: container.attributes.title, pos: newPos + 1, listLength: this.containerList.length} ); } }, moveItemDown(containerId) { const currentIndex = this.containerList.findIndex(container => container.id === containerId); if (this.containerList.length - 1 > currentIndex) { const container = this.containerById({id: containerId}); const newPos = currentIndex + 1; this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]); this.assistiveLive = this.$gettextInterpolate( this.$gettext('%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.') , {containerTitle: container.attributes.title, pos: newPos + 1, listLength: this.containerList.length} ); } }, abortKeyboardSorting(containerId) { const container = this.containerById({id: containerId}); this.keyboardSelected = null; this.assistiveLive = this.$gettextInterpolate( this.$gettext('%{containerTitle} Abschnitt, Neuordnung abgebrochen.') , {containerTitle: container.attributes.title} ); this.$emit('select', this.currentId); }, storeKeyboardSorting(containerId) { const container = this.containerById({id: containerId}); const currentIndex = this.containerList.findIndex(container => container.id === containerId); this.keyboardSelected = null; this.assistiveLive = this.$gettextInterpolate( this.$gettext('%{containerTitle} Abschnitt, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.') , {containerTitle: container.attributes.title, pos: currentIndex + 1, listLength: this.containerList.length} ); this.storeSort(); } }, created() { this.pluginManager.registerComponentsLocally(this); }, watch: { async structuralElement() { this.setCurrentElementId(this.structuralElement.id); this.initCurrent(); if (this.isTask) { this.loadTask({ taskId: this.structuralElement.relationships.task.data.id, }); } if (this.isLink) { this.loadStructuralElement(this.structuralElement.attributes['target-id']); } }, containers() { this.containerList = this.containers; }, containerList() { if (this.keyboardSelected) { this.$nextTick(() => { const selected = this.$refs['sortableHandle' + this.keyboardSelected][0]; selected.focus(); selected.scrollIntoView({behavior: "smooth", block: "center"}); }); } }, consumeMode(newState) { this.consumModeTrap = newState; }, }, // this line provides all the components to courseware plugins provide: () => ({ containerComponents: ContainerComponents, coursewarePluginComponents: CoursewarePluginComponents, }), }; </script>