@eclipse-scout/core
Version:
Eclipse Scout runtime
293 lines (257 loc) • 9.9 kB
text/typescript
/*
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {aria, DesktopNotificationEventMap, DesktopNotificationModel, Device, EnumObject, InitModelOf, Notification as ScoutNotification, scout, Status, StatusOrModel, strings} from '../../index';
export class DesktopNotification extends ScoutNotification implements DesktopNotificationModel {
declare model: DesktopNotificationModel;
declare eventMap: DesktopNotificationEventMap;
declare self: DesktopNotification;
duration: number;
removeTimeout: number;
nativeOnly: boolean;
nativeNotificationTitle: string;
nativeNotificationStatus: Status;
nativeNotificationVisibility: NativeNotificationVisibility;
nativeNotification: Notification;
nativeNotificationShown: boolean;
$loader: JQuery;
protected _removing: boolean;
constructor() {
super();
this.closable = true;
this.duration = 5000;
this.removeTimeout = null;
this._removing = false;
this.nativeOnly = false;
this.nativeNotificationTitle = null;
this.nativeNotificationStatus = null;
this.nativeNotificationVisibility = DesktopNotification.NativeNotificationVisibility.NONE;
this.nativeNotification = null;
this.nativeNotificationShown = false;
}
static NativeNotificationVisibility = {
/**
* No native notification is shown.
*/
NONE: 'none',
/**
* The native notification is only shown if the application is in background.
*/
BACKGROUND: 'background',
/**
* The native notification is always shown.
*/
ALWAYS: 'always'
} as const;
/**
* When duration is set to INFINITE, the notification is not removed automatically.
*/
static INFINITE = -1;
protected override _init(model: InitModelOf<this>) {
super._init(model);
// A desktop notification belongs to the desktop and should stay visible until it is closed.
this.setParent(this.session.desktop);
this.setOwner(this.parent);
let defaults = this.session.desktop.nativeNotificationDefaults;
if (defaults) {
this.nativeNotificationTitle = model.nativeNotificationTitle !== undefined ? model.nativeNotificationTitle : defaults.title;
if (this.nativeNotificationStatus) {
this.nativeNotificationStatus.iconId = this.nativeNotificationStatus.iconId !== undefined ? this.nativeNotificationStatus.iconId : defaults.iconId;
} else {
this.nativeNotificationStatus = new Status({
iconId: defaults.iconId
});
}
this.nativeNotificationVisibility = scout.nvl(model.nativeNotificationVisibility !== undefined ? model.nativeNotificationVisibility : defaults.visibility, DesktopNotification.NativeNotificationVisibility.NONE);
}
this.resolveTextKeys(['nativeNotificationTitle']);
}
protected override _render() {
this._initNativeNotification();
this.$container = this.$parent.prependDiv('desktop-notification');
this.$content = this.$container.appendDiv('desktop-notification-content');
this.$messageText = this.$content.appendDiv('desktop-notification-message');
this.$loader = this.$container.appendDiv('desktop-notification-loader');
aria.role(this.$container, 'alert'); // Read the notification to screen reader users
if (Device.get().supportsCssAnimation()) {
this.$loader.addClass('animated');
}
if (this.nativeOnly) {
this.setVisible(false);
}
}
protected override _renderLoading() {
this.$container.toggleClass('loading', this.loading);
this.$loader.setVisible(this.loading);
}
protected override _destroy() {
if (this.nativeNotification) {
// No need to keep the native notification open if the regular one is closed (relevant if the user actively closes it)
this.nativeNotification.close();
}
super._destroy();
}
/** @internal */
_isDocumentHidden(): boolean {
return document.hidden;
}
protected _showNativeNotification(permission: NotificationPermission) {
if (permission === 'denied' || permission === 'default') {
if (this.nativeOnly) {
// See comment in _initNativeNotification
this.hide();
}
return;
}
let title = scout.nvl(this.nativeNotificationTitle, '');
let body = (this.nativeNotificationStatus || {}).message;
if (strings.empty(body)) {
body = (this.status || {}).message;
}
if (!body) {
body = '';
}
if (this.htmlEnabled) {
body = strings.plainText(body, {removeFontIcons: true});
}
let iconId = (this.nativeNotificationStatus || {}).iconId;
if (strings.empty(iconId)) {
// icon must not be null or empty. If no icon it must be undefined
iconId = undefined;
}
this.nativeNotification = new Notification(title, {
body: body,
icon: iconId
});
this.nativeNotification.addEventListener('show', event => {
this._setNativeNotificationShown(true);
});
this.nativeNotification.addEventListener('click', event => {
window.focus();
});
// Native notifications are closed when the regular notification is closed (either by the user, the timeout or programmatically)
this.nativeNotification.addEventListener('close', event => {
if (this.nativeOnly) {
// Only close it if nativeOnly is true.
// If nativeOnly is false, clicking the notification should reveal the app incl. the original notification which could contain more information (e.g. a link).
this.hide();
}
this.nativeNotification = null;
this._setNativeNotificationShown(false);
});
}
protected _initNativeNotification() {
if (this.nativeNotificationShown) {
// Don't show the same notification twice (could happen if the user reloads the page and the notification is still open. Especially important for nativeOnly with infinite duration).
return;
}
if (this.nativeNotificationVisibility === DesktopNotification.NativeNotificationVisibility.NONE) {
this._hideLaterIfNativeOnly();
return;
}
if (this.nativeNotificationVisibility === DesktopNotification.NativeNotificationVisibility.BACKGROUND && !this._isDocumentHidden()) {
this._hideLaterIfNativeOnly();
return;
}
if (!window.Notification || Notification.permission === 'denied') {
this._hideLaterIfNativeOnly();
return;
}
if (this._checkNotificationPromise()) {
Notification.requestPermission().then(this._showNativeNotification.bind(this));
} else {
// noinspection JSIgnoredPromiseFromCall
Notification.requestPermission(this._showNativeNotification.bind(this));
}
}
protected _hideLaterIfNativeOnly() {
if (!this.nativeOnly) {
return;
}
// If native notifications are not shown, there is no need to keep the (invisible) desktop notification open (prevent dom-leak)
setTimeout(() => this.hide()); // async because this method is called in render and removing the notification within render throws exception
}
/**
* Checks if browser supports the promise-based version of the method requestPermission. Safari only supports the older callback version.
*/
protected _checkNotificationPromise(): boolean {
try {
Notification.requestPermission().then();
} catch (e) {
return false;
}
return true;
}
protected override _onCloseIconClick() {
this.hide();
}
/**
* Displays the notification by adding it to the desktop and rendering it.
*/
show() {
this.session.desktop.addNotification(this);
}
/**
* Closes the notification by removing it from the desktop and destroying it. Also triggers a close event.
*/
hide() {
if (this._removing) {
return;
}
this.trigger('close');
this.session.desktop.removeNotification(this);
}
fadeIn($parent: JQuery) {
this.render($parent);
if (!Device.get().supportsCssAnimation()) {
return;
}
this.$container.addClassForAnimation('desktop-notification-slide-in');
}
fadeOut() {
// prevent fadeOut from running more than once (for instance from the click of a user) and do nothing if it is already being destroyed.
if (this._removing || this.destroying) {
return;
}
this._removing = true;
if (!Device.get().supportsCssAnimation() || !this.rendered) {
this.destroy();
return;
}
if (!this.$container.isVisible()) {
// Destroy immediately if it is invisible because the animationend event would not be triggered (is the case if nativeOnly is true)
this.destroy();
return;
}
this.$container.addClass('desktop-notification-fade-out');
this.$container.oneAnimationEnd(() => {
this.destroy();
});
}
override invalidateLayoutTree() {
// called by notification.js. Since desktop notification has no htmlComp, no need to invalidate
}
protected _setNativeNotificationShown(shown: boolean) {
this._setProperty('nativeNotificationShown', shown);
}
setNativeNotificationTitle(title: string) {
this.setProperty('nativeNotificationTitle', title);
}
setNativeNotificationStatus(status: StatusOrModel) {
this.setProperty('nativeNotificationStatus', status);
}
protected _setNativeNotificationStatus(status: StatusOrModel) {
status = Status.ensure(status);
this._setProperty('nativeNotificationStatus', status);
}
setNativeNotificationVisibility(visibility: NativeNotificationVisibility) {
this.setProperty('nativeNotificationVisibility', visibility);
}
}
export type NativeNotificationVisibility = EnumObject<typeof DesktopNotification.NativeNotificationVisibility>;