Skip to content
Snippets Groups Projects
Commit 27ea0ad1 authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms
Browse files

fix notification timer display and removal, fixes #4327

Closes #4327

Merge request studip/studip!3130
parent 6b15e09a
No related branches found
No related tags found
No related merge requests found
...@@ -39,6 +39,9 @@ Vue.mixin({ ...@@ -39,6 +39,9 @@ Vue.mixin({
globalOn(...args) { globalOn(...args) {
eventBus.on(...args); eventBus.on(...args);
}, },
globalOff(...args) {
eventBus.off(...args);
},
getStudipConfig: store.getters['studip/getConfig'] getStudipConfig: store.getters['studip/getConfig']
}, },
}); });
......
...@@ -122,6 +122,10 @@ ...@@ -122,6 +122,10 @@
width: 0; width: 0;
} }
&.system-notification-disrupted .system-notification-timeout {
display: none;
}
a:not(.system-notification-message) { a:not(.system-notification-message) {
color: var(--black); color: var(--black);
text-decoration-line: underline; text-decoration-line: underline;
......
<template> <template>
<transition name="system-notification-slide" appear> <div v-cloak
<div v-if="showMe" class="system-notification"
:class="'system-notification system-notification-' + notification.type" :class="cssClasses"
@mouseover="disruptTimeout" @mouseover="disruptTimeout"
@mouseout="initTimeout" @mouseout="initTimeout"
@focus="disruptTimeout" @focus="disruptTimeout"
@blur="initTimeout"> @blur="initTimeout"
<div class="system-notification-icon"> >
<studip-icon :shape="icon.shape" <div class="system-notification-icon">
:size="48" <studip-icon :shape="icon.shape"
:role="icon.color" :size="48"
alt="" :role="icon.color"
title=""></studip-icon> alt=""
</div> title=""></studip-icon>
<div class="system-notification-content">
<p v-html="notification.message"></p>
<p class="sr-only" v-if="hasTimeout">
{{ $gettext('Strg+Alt+T hält das automatische Ausblenden der Meldung an bzw. setzt es wieder fort.') }}
</p>
<details v-if="notification.details?.length > 0"
class="system-notification-details">
<summary>
{{ $gettext('Details') }}
</summary>
<template v-if="Array.isArray(notification.details)">
<p v-for="(detail, index) in notification.details"
:key="index"
v-html="detail"></p>
</template>
<p v-else v-html="notification.details"></p>
</details>
</div>
<button v-if="allowClosing"
class="system-notification-close undecorated"
:title="$gettext('Diese Meldung schließen')"
@click.prevent="destroyMe"
@keydown.space="destroyMe"
tabindex="0">
<studip-icon shape="decline"
:size="20"
class="close-system-notification"/>
</button>
<transition v-if="hasTimeout"
name="system-notification-timeout"
appear>
<div v-if="!stopTimeout"
class="system-notification-timeout"
ref="timeout-counter"></div>
</transition>
</div> </div>
</transition> <div class="system-notification-content">
<p v-html="notification.message"></p>
<p class="sr-only" v-if="hasTimeout">
{{ $gettext('Strg+Alt+T hält das automatische Ausblenden der Meldung an bzw. setzt es wieder fort.') }}
</p>
<details v-if="notification.details?.length > 0"
class="system-notification-details">
<summary>
{{ $gettext('Details') }}
</summary>
<template v-if="Array.isArray(notification.details)">
<p v-for="(detail, index) in notification.details"
:key="index"
v-html="detail"></p>
</template>
<p v-else v-html="notification.details"></p>
</details>
</div>
<button v-if="allowClosing"
class="system-notification-close undecorated"
:title="$gettext('Diese Meldung schließen')"
@click.prevent="destroyMe"
@keydown.space="destroyMe"
tabindex="0">
<studip-icon shape="decline"
:size="20"
class="close-system-notification"/>
</button>
<transition v-if="hasTimeout"
name="system-notification-timeout"
appear
>
<div v-if="!stopTimeout"
class="system-notification-timeout"
ref="timeout-counter"></div>
</transition>
</div>
</template> </template>
<script> <script>
export default { export default {
name: 'SystemNotification', name: 'SystemNotification',
props: { props: {
allowClosing: {
type: Boolean,
default: true
},
appendTo: {
type: String,
default: null
},
notification: { notification: {
type: Object, type: Object,
required: true required: true
...@@ -63,23 +72,26 @@ export default { ...@@ -63,23 +72,26 @@ export default {
visibleFor: { visibleFor: {
type: Number, type: Number,
default: 5000 default: 5000
},
appendTo: {
type: String,
default: null
},
allowClosing: {
type: Boolean,
default: true
} }
}, },
data() { data() {
return { return {
showMe: false, stopTimeout: false,
stopTimeout: false timeout: null,
windowIsBlurred: false,
} }
}, },
computed: { computed: {
cssClasses() {
const classes = [`system-notification-${this.notification.type}`];
if (this.isDisrupted) {
classes.push('system-notification-disrupted');
}
return classes;
},
hasTimeout() {
return !['exception', 'error'].includes(this.notification.type);
},
icon() { icon() {
let iconShape = 'info-circle'; let iconShape = 'info-circle';
let iconColor = 'info'; let iconColor = 'info';
...@@ -101,29 +113,28 @@ export default { ...@@ -101,29 +113,28 @@ export default {
iconColor = 'status-green'; iconColor = 'status-green';
break; break;
} }
return { shape: iconShape, color: iconColor }; return {shape: iconShape, color: iconColor};
}, },
hasTimeout() { isDisrupted() {
return !['exception', 'error'].includes(this.notification.type); return this.timeout !== null && this.stopTimeout;
} }
}, },
methods: { methods: {
initTimeout() { destroyMe() {
if (this.hasTimeout && this.visibleFor > 0) { this.$emit('destroyMe');
this.stopTimeout = false;
setTimeout(() => {
if (!this.stopTimeout) {
this.destroyMe();
}
}, this.visibleFor);
}
}, },
disruptTimeout() { disruptTimeout() {
this.stopTimeout = true; this.stopTimeout = true;
clearTimeout(this.timeout);
}, },
destroyMe() { initTimeout() {
this.showMe = false; if (this.hasTimeout && this.visibleFor > 0) {
this.$emit('destroyMe'); this.stopTimeout = false;
this.timeout = setTimeout(
() => this.destroyMe(),
this.visibleFor
);
}
} }
}, },
mounted() { mounted() {
...@@ -139,18 +150,24 @@ export default { ...@@ -139,18 +150,24 @@ export default {
} }
} }
this.initTimeout();
this.globalOn('disrupt-system-notifications', this.disruptTimeout);
this.globalOn('resume-system-notifications', this.initTimeout);
if (!STUDIP.config?.PERSONAL_NOTIFICATIONS_AUDIO_DEACTIVATED) { if (!STUDIP.config?.PERSONAL_NOTIFICATIONS_AUDIO_DEACTIVATED) {
const audio = new Audio(STUDIP.ASSETS_URL + '/sounds/blubb.mp3'); const audio = new Audio(STUDIP.ASSETS_URL + '/sounds/blubb.mp3');
audio.play(); audio.play();
} }
this.showMe = true; },
destroyed() {
this.initTimeout(); this.globalOff('disrupt-system-notifications', this.disruptTimeout);
window.addEventListener('keydown', evt => { this.globalOff('resume-system-notifications', this.initTimeout);
if (evt.altKey && evt.ctrlKey && evt.code === 'KeyT') {
this.stopTimeout ? this.initTimeout() : this.disruptTimeout();
}
})
} }
} }
</script> </script>
<style scoped>
[v-cloak] {
display: none;
}
</style>
<template> <template>
<div role="alert" <transition-group name="system-notification-slide"
:class="'system-notifications ' + (placement === 'topcenter' ? 'top-center' : 'bottom-right')"> :class="'system-notifications ' + (placement === 'topcenter' ? 'top-center' : 'bottom-right')"
<system-notification v-for="(notification, index) in allNotifications" tag="div"
:key="'message-' + index" role="alert"
:notification="notification"></system-notification> appear
</div> >
<system-notification v-for="notification in allNotifications"
:key="`message-${notification.key}`"
:notification="notification"
@destroyMe="destroyNotification(notification)"
></system-notification>
</transition-group>
</template> </template>
<script> <script>
...@@ -14,8 +20,9 @@ export default { ...@@ -14,8 +20,9 @@ export default {
name: 'SystemNotificationManager', name: 'SystemNotificationManager',
components: { SystemNotification }, components: { SystemNotification },
props: { props: {
appendAllTo: String,
notifications: { notifications: {
type: Array, type: [Array, Object],
default: () => [] default: () => []
}, },
placement: { placement: {
...@@ -24,28 +31,42 @@ export default { ...@@ -24,28 +31,42 @@ export default {
validator: value => { validator: value => {
return ['topcenter', 'bottomright'].includes(value); return ['topcenter', 'bottomright'].includes(value);
} }
},
appendAllTo: {
type: String,
default: null
} }
}, },
data() { data() {
return { return {
allNotifications: this.notifications allNotifications: [],
counter: 0,
stoppedNotifications: false
} }
}, },
methods: { methods: {
getNotifications(type) { destroyNotification(notification) {
return this.allNotifications.filter((n) => n.type === type); this.allNotifications = this.allNotifications.filter(n => n !== notification);
}, }
destroyNotification(type, index) { },
created() {
if (Array.isArray(this.notifications)) {
this.allNotifications = [...this.notifications];
} else {
this.allNotifications = Object.values(this.notifications);
} }
}, },
mounted() { mounted() {
this.globalOn('push-system-notification', notification => { this.globalOn('push-system-notification', notification => {
this.allNotifications.push(notification); this.allNotifications.push({
key: this.counter++,
...notification
});
});
window.addEventListener('keydown', evt => {
if (evt.altKey && evt.ctrlKey && evt.code === 'KeyT') {
this.stoppedNotifications = !this.stoppedNotifications;
const event = this.stoppedNotifications ? 'disrupt-system-notifications' : 'resume-system-notifications';
this.globalEmit(event);
}
}); });
} }
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment