Skip to content
5 changes: 3 additions & 2 deletions Framework/Frontend/css/src/bootstrap.css
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,9 @@ label { display: block; margin-bottom: 0.5em; }

.notification { position: absolute; top: 2em; width: 100%; text-align: center; cursor: pointer; pointer-events: none; }
.notification-content { transition: opacity 0.8s cubic-bezier(0.18, 0.89, 0.34, 1.11), filter 0.8s cubic-bezier(0.18, 0.89, 0.34, 1.11); }
.notification-open { display: inline; opacity: 1; filter: blur(0); pointer-events: all; }
.notification-close { display: inline; opacity: 0; filter: blur(5em); pointer-events: none; }
.notification-open { display: inline-flex; opacity: 1; filter: blur(0); pointer-events: all; }
.notification-close { display: inline-flex; opacity: 0; filter: blur(5em); pointer-events: none; }
.notification-copy-btn { border-radius: 0 0.25em 0.25em 0; }

/* Basic table */

Expand Down
94 changes: 85 additions & 9 deletions Framework/Frontend/js/src/Notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import Observable from './Observable.js';
import { h } from './renderer.js';
import switchCase from './switchCase.js';
import { iconClipboard, iconCheck, iconCircleX } from './icons.js';

/**
* Container of notification with time management
Expand Down Expand Up @@ -48,7 +49,11 @@ export class Notification extends Observable {
this.message = '';
this.type = 'primary';
this.state = 'hidden'; // Shown, hidden
this.timerId = 0; // Timer to auto-hide notification
this.hideTimerId = null; // Timer to auto-hide notification
this.duration = 5000; // Original duration of the current notification
this.hovered = false; // Whether the notification is hovered
this.copyState = 'idle'; // 'idle' | 'copied' | 'failed' — state for the copy button
this.copyTimerId = null; // Timer to reset copyState to 'idle' after copy
}

/**
Expand All @@ -70,17 +75,22 @@ export class Notification extends Observable {

duration = duration || 5000;

// Clear previous message countdown
clearTimeout(this.timerId);
// Clear previous message countdowns
clearTimeout(this.hideTimerId);
clearTimeout(this.copyTimerId);

this.message = message;
this.type = type;
this.state = 'shown';
this.duration = duration;
this.copyState = 'idle';

// Auto-hide after duration
if (duration !== Infinity) {
this.timerId = setTimeout(() => {
this.hide();
this.hideTimerId = setTimeout(() => {
if (!this.hovered) {
this.hide();
}
}, duration);
}

Expand All @@ -92,11 +102,54 @@ export class Notification extends Observable {
* (by a click on notification for example)
*/
hide() {
clearTimeout(this.timerId);
clearTimeout(this.hideTimerId);
clearTimeout(this.copyTimerId);
this.state = 'hidden';
this.copyState = 'idle';

this.notify();
}

/**
* Restart the auto-hide countdown for the currently shown notification, reusing the
* stored duration.
* No-op when the notification is hidden or duration is Infinity.
*/
restartHideTimer() {
if (this.state !== 'shown') {
return;
}
clearTimeout(this.hideTimerId);
if (this.duration !== Infinity) {
this.hideTimerId = setTimeout(() => {
if (!this.hovered) {
this.hide();
}
}, this.duration);
}
}

/**
* Copy the current message to the clipboard and flash `copyState` to `'copied'` for 1.5s.
* If the clipboard write rejects, `copyState` flashes to `'failed'` instead.
* @return {Promise<void>}
*/
async copy() {
let nextState;
try {
await navigator.clipboard.writeText(this.message);
nextState = 'copied';
} catch {
nextState = 'failed';
}
clearTimeout(this.copyTimerId);
this.copyState = nextState;
this.notify();
this.copyTimerId = setTimeout(() => {
this.copyState = 'idle';
this.notify();
}, 1500);
}
}

/**
Expand Down Expand Up @@ -124,13 +177,36 @@ export class Notification extends Observable {
*/
export const notification = (notificationInstance) => h('.notification.text-no-select.level4.text-light', {

}, h('span.notification-content.br2.p2.shadow-level4', {
}, h('div.notification-content.br2.shadow-level4', {
// ClassName: notificationInstance.message && (notificationInstance.state === 'shown' ? 'notification-open' : 'notification-close'),
onclick: () => notificationInstance.hide(),
onmouseenter: () => {
notificationInstance.hovered = true;
},
onmouseleave: () => {
notificationInstance.hovered = false;
notificationInstance.restartHideTimer();
},
className: `${switchCase(notificationInstance.type, {
primary: 'white bg-primary',
success: 'white bg-success',
warning: 'white bg-warning',
danger: 'white bg-danger',
})} ${notificationInstance.state === 'shown' ? 'notification-open' : 'notification-close'}`,
}, notificationInstance.message));
}, [
h('div.mh2.pv2', { onclick: () => notificationInstance.hide() }, notificationInstance.message),
h(`button.btn.btn-${notificationInstance.type}.notification-copy-btn`, {
title: switchCase(notificationInstance.copyState, {
copied: 'Copied!',
failed: "Couldn't copy",
idle: 'Copy to clipboard',
}),
onclick: (e) => {
e.stopPropagation();
notificationInstance.copy();
},
}, switchCase(notificationInstance.copyState, {
copied: iconCheck(),
failed: iconCircleX(),
idle: iconClipboard(),
})),
]));
Loading
Loading