diff --git a/resources/assets/javascripts/bootstrap/beforeunload-event-listener.js b/resources/assets/javascripts/bootstrap/beforeunload-event-listener.js new file mode 100644 index 0000000000000000000000000000000000000000..4e966308cd0de441e06eca65ff1d9593debca1a5 --- /dev/null +++ b/resources/assets/javascripts/bootstrap/beforeunload-event-listener.js @@ -0,0 +1,6 @@ +STUDIP.domReady(() => { + // Before-unload event listener. + window.addEventListener('beforeunload', (e) => { + STUDIP.eventBus.emit('studip:beforeunload', e); + }, {capture: true}); +}); diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index fdff1c7a9387c53be4fe0c418384cf4bb88ba58c..09779ddb164d4f19f6db387e6715a5a31b770693 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -86,6 +86,7 @@ import "./bootstrap/admin-courses.js" import "./bootstrap/cache-admin.js" import "./bootstrap/oer.js" import "./bootstrap/courseware.js" +import "./bootstrap/beforeunload-event-listener.js" import "./mvv_course_wizard.js" import "./mvv.js" diff --git a/resources/vue/components/courseware/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/CoursewareAccordionContainer.vue index 8e4d0984d1aab3ccb4ff8ec19a30fd83c6bc7b90..92029c69b899ed0b26201b225b8c6e2b956b5770 100644 --- a/resources/vue/components/courseware/CoursewareAccordionContainer.vue +++ b/resources/vue/components/courseware/CoursewareAccordionContainer.vue @@ -8,6 +8,7 @@ @storeContainer="storeContainer" @closeEdit="initCurrentData" @sortBlocks="enableSort" + @setSortMode="setSortMode" > <template v-slot:containerContent> <courseware-collapsible-box @@ -244,6 +245,9 @@ export default { if (blockAdder.container !== undefined && blockAdder.container.id === this.container.id) { this.initCurrentData(); } + }, + setSortMode(mode) { + this.sortMode = mode; } }, watch: { diff --git a/resources/vue/components/courseware/CoursewareActionWidget.vue b/resources/vue/components/courseware/CoursewareActionWidget.vue index f4d690683aa4b0c1ea723a4af2dbc9f20a250306..89f27ea549e509cbc913c339c4682ca3e7a52d23 100644 --- a/resources/vue/components/courseware/CoursewareActionWidget.vue +++ b/resources/vue/components/courseware/CoursewareActionWidget.vue @@ -74,6 +74,12 @@ export default { SidebarWidget, }, mixins: [CoursewareExport], + data() { + return { + objectIsBlocked: false, + blockedFrom: '' + } + }, computed: { ...mapGetters({ userId: 'userId', @@ -86,6 +92,9 @@ export default { blockerId: 'currentElementBlockerId', blockedByThisUser: 'currentElementBlockedByThisUser', blockedByAnotherUser: 'currentElementBlockedByAnotherUser', + editDialogState: 'showStructuralElementEditDialog', + deleteDialogState: 'showStructuralElementDeleteDialog', + structuralElementSortMode: 'structuralElementSortMode' }), isRoot() { if (!this.structuralElement) { @@ -126,6 +135,7 @@ export default { companionInfo: 'companionInfo', addBookmark: 'addBookmark', lockObject: 'lockObject', + unlockObject: 'unlockObject', setConsumeMode: 'coursewareConsumeMode', setViewMode: 'coursewareViewMode', setShowToolbar: 'coursewareShowToolbar', @@ -141,6 +151,8 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; + this.blockedFrom = 'editDialogState'; } catch(error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -164,6 +176,8 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; + this.blockedFrom = 'structuralElementSortMode'; } catch (error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -188,6 +202,8 @@ export default { return false; } await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; + this.blockedFrom = 'deleteDialogState'; this.showElementDeleteDialog(true); }, addElement() { @@ -213,6 +229,60 @@ export default { }, linkElement() { this.showElementLinkDialog(true); + }, + async beforeUnloadActions() { + this.beforeUnloadCleanup(); + if (this.objectIsBlocked) { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + } + }, + beforeUnloadCleanup() { + // The following dialogs and elements must be set to be closed, in order to avoid lockObject conflicts. + this.showElementEditDialog(false); + this.setStructuralElementSortMode(false); + this.showElementDeleteDialog(false); + }, + async beforeUnloadHandler(event) { + if (this.objectIsBlocked) { + event.preventDefault(); + event.returnValue = 'There are unsaved changes, do you want to leave?'; + await this.beforeUnloadActions(); + return event.returnValue; + } + return null; + } + }, + mounted () { + STUDIP.eventBus.on('studip:beforeunload', this.beforeUnloadHandler); + }, + beforeDestroy () { + STUDIP.eventBus.off('studip:beforeunload', this.beforeUnloadHandler); + }, + watch: { + blockedByThisUser(newValue) { + if (!newValue) { + this.objectIsBlocked = false + } + }, + editDialogState(newValue) { + if (!newValue && this.blockedFrom == 'editDialogState') { + this.objectIsBlocked = false + } + }, + structuralElementSortMode(newValue) { + if (!newValue && this.blockedFrom == 'structuralElementSortMode') { + this.objectIsBlocked = false + } + }, + deleteDialogState(newValue) { + if (!newValue && this.blockedFrom == 'deleteDialogState') { + this.objectIsBlocked = false + } + }, + objectIsBlocked(newValue) { + if (!newValue) { + this.blockedFrom = ''; + } } }, }; diff --git a/resources/vue/components/courseware/CoursewareBlockEdit.vue b/resources/vue/components/courseware/CoursewareBlockEdit.vue index 55b8b3a1b9b06a4557b5ff4d16681f973b556e1e..f48a6b062805f3c1f7478c6f4604543cedd699ad 100644 --- a/resources/vue/components/courseware/CoursewareBlockEdit.vue +++ b/resources/vue/components/courseware/CoursewareBlockEdit.vue @@ -18,6 +18,7 @@ export default { name: 'courseware-block-edit', props: { block: Object, + objectIsBlocked: Boolean, }, data() { return { @@ -33,12 +34,28 @@ export default { this.$store.dispatch('coursewareBlockAdder', {}); this.$store.dispatch('coursewareShowToolbar', false); }, + beforeUnloadHandler(event) { + if (this.exitHandler || this.objectIsBlocked) { + event.preventDefault(); + this.autoSave(); + event.returnValue = 'There are unsaved changes, do you want to leave?'; + return event.returnValue; + } + return null; + }, + async autoSave() { + this.$emit('store'); + this.exitHandler = false; + } }, beforeDestroy() { if (this.exitHandler) { - console.log('autosave'); - this.$emit('store'); + this.autoSave(); } - } + STUDIP.eventBus.off('studip:beforeunload', this.beforeUnloadHandler); + }, + mounted () { + STUDIP.eventBus.on('studip:beforeunload', this.beforeUnloadHandler); + }, }; </script> diff --git a/resources/vue/components/courseware/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/CoursewareDefaultBlock.vue index 14ab304ec61394975a38038b5cceb7db59b517ba..6a9ad39f9ab73a4a5a8437f0f1268d1e2986d845 100644 --- a/resources/vue/components/courseware/CoursewareDefaultBlock.vue +++ b/resources/vue/components/courseware/CoursewareDefaultBlock.vue @@ -36,6 +36,7 @@ <courseware-block-edit v-if="canEdit && showEdit" :block="block" + :objectIsBlocked="objectIsBlocked" @store="prepareStoreEdit" @close="closeEdit" > @@ -130,6 +131,7 @@ export default { textDeleteAlert: this.$gettext('Möchten Sie diesen Block wirklich löschen?'), textRemoveLockTitle: this.$gettext('Sperre aufheben'), textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Blocks wirklich aufheben?'), + objectIsBlocked: false, }; }, computed: { @@ -217,6 +219,7 @@ export default { await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } }); if (!this.blocked) { await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = true; if (!this.preview) { this.showContent = false; } @@ -264,15 +267,23 @@ export default { } if (this.blockerId === null) { await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = true; this.$emit('storeEdit'); } + + // To make sure the object is not blocked. + if (this.objectIsBlocked || this.blockedByThisUser) { + await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = false; + } }, async closeEdit() { await this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // has block editor lock changed? this.displayFeature(false); this.$emit('closeEdit'); - if (this.blockedByThisUser) { + if (this.blockedByThisUser || this.blockedByThisUser) { await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = false; } this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // to update block editor lock }, @@ -280,6 +291,7 @@ export default { await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } }); if (!this.blocked) { await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = true; this.showDeleteDialog = true; } else { if (this.blockedByThisUser) { @@ -298,6 +310,7 @@ export default { await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } }); if (this.blockedByThisUser) { await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = false; } this.showDeleteDialog = false; }, @@ -323,6 +336,7 @@ export default { // lock parent container await this.lockObject({ id: containerId, type: 'courseware-containers' }); + this.objectIsBlocked = true; // update container information for (let i = 0; i < sections.length; i++) { for (let j = 0; j < sections[i].blocks.length; j++) { @@ -337,6 +351,7 @@ export default { await this.updateContainer({ container, structuralElementId }); // unlock container await this.unlockObject({ id: containerId, type: 'courseware-containers' }); + this.objectIsBlocked = false; await this.loadContainer(containerId); this.deleteBlock({ blockId: this.block.id, @@ -348,14 +363,21 @@ export default { }, async executeRemoveLock() { await this.unlockObject({ id: this.block.id , type: 'courseware-blocks' }); + this.objectIsBlocked = false; await this.loadBlock({ id: this.block.id }); this.showRemoveLockDialog = false; + }, + async performUnloadObject() { + await this.unlockObject({ id: this.block.id , type: 'courseware-blocks' }); + this.objectIsBlocked = false; } }, watch: { showEdit(state) { - this.$emit('showEdit', state); + if (state) { + this.objectIsBlocked = true; + } } } }; diff --git a/resources/vue/components/courseware/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/CoursewareDefaultContainer.vue index 70a6da708f0f66215174ea40408d847e2b221fe6..38f6b09863cb93eb718535e41a0f1592833f55c0 100644 --- a/resources/vue/components/courseware/CoursewareDefaultContainer.vue +++ b/resources/vue/components/courseware/CoursewareDefaultContainer.vue @@ -93,6 +93,7 @@ export default { textDeleteAlert: this.$gettext('Möchten Sie diesen Abschnitt wirklich löschen?'), textRemoveLockTitle: this.$gettext('Sperre aufheben'), textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Abschnitts wirklich aufheben?'), + objectIsBlocked: false, }; }, computed: { @@ -147,6 +148,7 @@ export default { } await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = true; this.showEditDialog = true; }, async closeEdit() { @@ -155,6 +157,7 @@ export default { this.showEditDialog = false; if (this.blockedByThisUser) { await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = false; } await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); }, @@ -174,6 +177,7 @@ export default { } if (this.blockerId === null) { await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = true; this.$emit('storeContainer'); } this.showEditDialog = false; @@ -182,6 +186,7 @@ export default { await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); if (!this.blocked) { await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = true; this.showDeleteDialog = true; } else { if (this.blockedByThisUser) { @@ -200,6 +205,7 @@ export default { await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); if (this.blockedByThisUser) { await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = false; } this.showDeleteDialog = false; }, @@ -231,6 +237,7 @@ export default { return false; } await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = true; this.$emit('sortBlocks'); }, displayRemoveLockDialog() { @@ -238,10 +245,37 @@ export default { }, async executeRemoveLock() { await this.unlockObject({ id: this.container.id , type: 'courseware-containers' }); + this.objectIsBlocked = false; await this.loadContainer({ id: this.container.id }); this.showRemoveLockDialog = false; }, - + async beforeUnloadActions() { + this.beforeUnloadCleanup(); + if (this.blockedByThisUser || this.objectIsBlocked) { + await this.unlockObject({ id: this.container.id , type: 'courseware-containers' }); + } + }, + beforeUnloadCleanup() { + // The following dialogs and elements must be set to be closed, in order to avoid lockObject conflicts. + this.showEditDialog = false; + this.showDeleteDialog = false; + this.$emit('setSortMode', false); + }, + async beforeUnloadHandler(event) { + if (this.blockedByThisUser || this.objectIsBlocked) { + event.preventDefault(); + event.returnValue = 'There are unsaved changes, do you want to leave?'; + await this.beforeUnloadActions(); + return event.returnValue; + } + return null; + } + }, + mounted () { + STUDIP.eventBus.on('studip:beforeunload', this.beforeUnloadHandler); + }, + beforeDestroy () { + STUDIP.eventBus.off('studip:beforeunload', this.beforeUnloadHandler); }, watch: { diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue index ae0f9a1993f74fc30ae7d02b67c0ceb8c985a5fa..8c12b88976218c5c5728cf58ff2269c46464351d 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue @@ -732,6 +732,7 @@ export default { 'expire-date': '' }, deletingPreviewImage: false, + objectIsBlocked: false, }; }, @@ -1348,7 +1349,8 @@ export default { this.uploadFileError = ''; this.deletingPreviewImage = false; }, - async menuAction(action) { + async menuAction(action, type = 'open') { + this.lastAction = action; switch (action) { case 'removeLock': this.displayRemoveLockDialog(); @@ -1362,6 +1364,7 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; } catch(error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -1393,6 +1396,7 @@ export default { return false; } await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; this.showElementDeleteDialog(true); break; case 'showInfo': @@ -1419,6 +1423,7 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; } catch (error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -1439,6 +1444,7 @@ export default { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByThisUser) { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; await this.loadStructuralElement(this.currentElement.id); } this.showElementEditDialog(false); @@ -1476,6 +1482,7 @@ export default { } if (!this.blocked) { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; } const file = this.$refs?.upload_image?.files[0]; if (file) { @@ -1512,6 +1519,7 @@ export default { id: this.currentId, }); await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; this.$emit('select', this.currentId); this.initCurrent(); }, @@ -1520,7 +1528,7 @@ export default { this.setStructuralElementSortMode(true); }, - storeSort() { + async storeSort() { this.setStructuralElementSortMode(false); this.sortContainersInStructualElements({ @@ -1528,11 +1536,19 @@ export default { containers: this.containerList, }); this.$emit('select', this.currentId); + if (this.blockedByThisUser) { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; + } }, - resetSort() { + async resetSort() { this.setStructuralElementSortMode(false); this.containerList = this.containers; + if (this.blockedByThisUser) { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; + } }, async exportCurrentElement(data) { @@ -1580,6 +1596,7 @@ export default { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByThisUser) { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; } this.showElementDeleteDialog(false); }, @@ -1641,7 +1658,7 @@ export default { this.companionSuccess({ info: this.$gettextInterpolate( - this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), + this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), { pageTitle: newElement.attributes.title } ) }); @@ -1709,13 +1726,42 @@ export default { }, async executeRemoveLock() { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; await this.loadStructuralElement(this.currentElement.id); this.showElementRemoveLockDialog(false); + }, + async beforeUnloadActions() { + this.beforeUnloadCleanup(); + if (this.blockedByThisUser && this.blocked || this.objectIsBlocked) { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + } + }, + beforeUnloadCleanup() { + // The following dialogs and elements must be set to be closed, in order to avoid lockObject conflicts. + this.showElementEditDialog(false); + this.showElementDeleteDialog(false); + this.setStructuralElementSortMode(false); + this.showElementAddDialog(false); + }, + async beforeUnloadHandler(event) { + if ((this.blockedByThisUser && this.blocked) || this.objectIsBlocked) { + event.preventDefault(); + event.returnValue = 'There are unsaved changes, do you want to leave?'; + await this.beforeUnloadActions(); + return event.returnValue; + } + return null; } }, created() { this.pluginManager.registerComponentsLocally(this); }, + mounted () { + STUDIP.eventBus.on('studip:beforeunload', this.beforeUnloadHandler); + }, + beforeDestroy () { + STUDIP.eventBus.off('studip:beforeunload', this.beforeUnloadHandler); + }, watch: { async structuralElement() {