diff --git a/Framework/Frontend/css/src/bootstrap.css b/Framework/Frontend/css/src/bootstrap.css index c5415d655..a5b256474 100644 --- a/Framework/Frontend/css/src/bootstrap.css +++ b/Framework/Frontend/css/src/bootstrap.css @@ -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 */ diff --git a/Framework/Frontend/js/src/Notification.js b/Framework/Frontend/js/src/Notification.js index b70ce520e..ca92d1e04 100644 --- a/Framework/Frontend/js/src/Notification.js +++ b/Framework/Frontend/js/src/Notification.js @@ -15,6 +15,9 @@ import Observable from './Observable.js'; import { h } from './renderer.js'; import switchCase from './switchCase.js'; +import { iconClipboard } from './icons.js'; + +const COPY_CONFIRMATION = 'Text copied to clipboard'; /** * Container of notification with time management @@ -49,6 +52,8 @@ export class Notification extends Observable { this.type = 'primary'; this.state = 'hidden'; // Shown, hidden this.timerId = 0; // Timer to auto-hide notification + this.duration = 5000; // Original duration of the current notification + this.hovered = false; // Whether the notification is hovered } /** @@ -76,11 +81,14 @@ export class Notification extends Observable { this.message = message; this.type = type; this.state = 'shown'; + this.duration = duration; // Auto-hide after duration if (duration !== Infinity) { this.timerId = setTimeout(() => { - this.hide(); + if (!this.hovered) { + this.hide(); + } }, duration); } @@ -124,13 +132,32 @@ 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; + // If this is not present then will show again when clicked close because of mouseLeave event + if (notificationInstance.state === 'shown') { + notificationInstance.show(notificationInstance.message, notificationInstance.type, notificationInstance.duration); + } + }, 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), + notificationInstance.message !== COPY_CONFIRMATION && h(`button.btn.btn-${notificationInstance.type}.notification-copy-btn`, { + title: 'Copy to clipboard', + onclick: (e) => { + e.stopPropagation(); + navigator.clipboard.writeText(notificationInstance.message) + .then(() => notificationInstance.show(COPY_CONFIRMATION, notificationInstance.type, 1500)); + }, + }, iconClipboard()), +]));