diff --git a/assets/css/style.css b/assets/css/style.css index 73413f6..7846dfc 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -564,6 +564,10 @@ a:hover { /* ----------------------- */ /* Keep camelCase names as they are from the library */ +#hot-container { + z-index: 7; +} + .handsontable.table-menu-btn .changeType { background: #e2e2e2; border-radius: 100%; diff --git a/src/Editor/CressTable.ts b/src/Editor/CressTable.ts index 8c02297..48cf816 100644 --- a/src/Editor/CressTable.ts +++ b/src/Editor/CressTable.ts @@ -4,6 +4,8 @@ import { ImageHandler } from './ImageHandler'; import { ExportHandler } from './ExportHandler'; import { ColumnTools } from './ColumnTools'; import { updateAttachment } from '../Dashboard/Storage'; +import { setSavedStatus } from '../utils/Unsaved'; +import * as Notification from '../utils/Notification'; export class CressTable { private table: Handsontable; @@ -64,6 +66,7 @@ export class CressTable { className: 'table-menu-btn', licenseKey: 'non-commercial-and-evaluation', afterChange() { + setSavedStatus(false); this.validateCells(); }, beforeValidate: (value) => this.setProcessStatus(value), @@ -99,9 +102,23 @@ export class CressTable { const result = await updateAttachment(id, [inputHeader, ...body]); if (result) { - // TODO: notification + setSavedStatus(true); + Notification.queueNotification('Saved', 'success'); } else { - // TODO: notification + Notification.queueNotification('Save failed', 'error'); + } + }); + + document.body.addEventListener('keydown', async (evt) => { + if (evt.key === 's') { + const result = await updateAttachment(id, [inputHeader, ...body]); + + if (result) { + setSavedStatus(true); + Notification.queueNotification('Saved', 'success'); + } else { + Notification.queueNotification('Save failed', 'error'); + } } }); } diff --git a/src/Types.ts b/src/Types.ts index 664f65a..0bc296c 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -59,3 +59,5 @@ export type validationStatus = 'unknown' | 'processing' | 'done'; /** An element from any DOM queries */ export type HTMLSVGElement = HTMLElement & SVGSVGElement; + +export type NotificationType = 'default' | 'error' | 'warning' | 'success'; diff --git a/src/utils/Notification.ts b/src/utils/Notification.ts new file mode 100644 index 0000000..15a0bb3 --- /dev/null +++ b/src/utils/Notification.ts @@ -0,0 +1,131 @@ +import { NotificationType } from '../Types'; +import { v4 as uuidv4 } from 'uuid'; + +const notifications: Notification[] = new Array(0); +let currentModeMessage: Notification = null; +const NUMBER_TO_DISPLAY = 3; // Number of notifications to display at a time. +const TIMEOUT = 5000; // Number of notifications to display at a time. + +const notificationIcon: Record = { + default: '', + warning: '⚠️ ', + error: '🔴 ', + success: '✅ ', +}; + +/** + * A class to manage cress notifications. + */ +export class Notification { + message: string; + displayed: boolean; + id: string; + isModeMessage: boolean; + logInfo: string; + timeoutID: number; + type: NotificationType; + /** + * Create a new notification. + * @param message - Notification content. + */ + constructor(message: string, type: NotificationType, logInfo: string = null) { + this.message = notificationIcon[type] + message; + this.displayed = false; + this.id = uuidv4(); + this.isModeMessage = message.search('Mode') !== -1; + this.logInfo = logInfo; + this.timeoutID = -1; + this.type = type; + } + + /** Set the ID from setTimeout. */ + setTimeoutId(id: number): void { + this.timeoutID = Math.max(id, -1); + } + + /** Display the Notification. */ + display(): void { + this.displayed = true; + } + + /** + * @returns The UUID for this notification. + */ + getId(): string { + return this.id; + } +} + +/** + * Clear the notification + * @param currentId - The ID of the notification to be cleared. + */ +function clearNotification(currentId: string): void { + if (document.getElementById(currentId)) { + document.getElementById(currentId).remove(); + } +} + +/** + * Display a notification. + * @param notification - Notification to display. + */ +function displayNotification(notification: Notification): void { + // Not sure what it does, maybe related to rodan/cress + if (notification.isModeMessage) { + if (currentModeMessage === null) { + currentModeMessage = notification; + } else { + window.clearTimeout(currentModeMessage.timeoutID); + return; + } + } + + // Remove the top notification if exceeds maxmimum + notifications.push(notification); + if (notifications.length > NUMBER_TO_DISPLAY) { + const toRemove = notifications.shift(); + clearNotification(toRemove.getId()); + } + const notificationContent = document.getElementById('notification-content'); + const newNotification = document.createElement('div'); + newNotification.classList.add('cress-notification'); + newNotification.classList.add(`cress-notification-${notification.type}`); + newNotification.id = notification.getId(); + newNotification.innerHTML = notification.message; + notificationContent.append(newNotification); + notificationContent.style.display = ''; + notification.display(); +} + +/** + * Start displaying notifications. Called automatically. + */ +function startNotification(notification: Notification): void { + displayNotification(notification); + notification.setTimeoutId( + window.setTimeout(clearNotification, TIMEOUT, notification.getId()), + ); + document + .getElementById(notification.getId()) + .addEventListener('click', () => { + window.clearTimeout(notification.timeoutID); + clearNotification(notification.getId()); + }); +} + +/** + * Add a notification to the queue. + * @param notification - Notification content. + */ +export function queueNotification( + notificationContent: string, + type: NotificationType = 'default', + logInfo: string = null, +): void { + const notification = new Notification(notificationContent, type, logInfo); + + startNotification(notification); +} + +export default { queueNotification };