Ubuntu
/*!
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
import {
DeepReadonly,
reactive,
createVNode,
readonly,
} from 'vue';
import NotificationComponent from './Notification.vue';
import Matomo from '../Matomo/Matomo';
import createVueApp from '../createVueApp';
import Notification from './Notification';
interface NotificationsData {
notifications: Notification[];
}
const { $ } = window;
class NotificationsStore {
private privateState = reactive({
notifications: [],
});
private nextNotificationId = 0;
get state(): DeepReadonly {
return readonly(this.privateState);
}
appendNotification(notification: Notification): void {
this.checkMessage(notification.message);
// remove existing notification before adding
if (notification.id) {
this.remove(notification.id);
}
this.privateState.notifications.push(notification);
}
prependNotification(notification: Notification): void {
this.checkMessage(notification.message);
// remove existing notification before adding
if (notification.id) {
this.remove(notification.id);
}
this.privateState.notifications.unshift(notification);
}
/**
* Removes a previously shown notification having the given notification id.
*/
remove(id: string): void {
this.privateState.notifications = this.privateState.notifications.filter(
(n) => n.id !== id,
);
}
parseNotificationDivs(): void {
const $notificationNodes = $('[data-role="notification"]');
const notificationsToShow: Notification[] = [];
$notificationNodes.each((index: number, notificationNode: HTMLElement) => {
const $notificationNode = $(notificationNode);
const attributes = $notificationNode.data();
const message = $notificationNode.html();
if (message) {
notificationsToShow.push({ ...attributes, message, animate: false } as Notification);
}
$notificationNodes.remove();
});
notificationsToShow.forEach((n) => this.show(n));
}
clearTransientNotifications(): void {
this.privateState.notifications = this.privateState.notifications.filter(
(n) => n.type !== 'transient',
);
}
/**
* Creates a notification and shows it to the user.
*/
show(notification: Notification): string {
this.checkMessage(notification.message);
let addMethod = notification.prepend ? this.prependNotification : this.appendNotification;
let notificationPosition: Notification['placeat'] = '#notificationContainer';
if (notification.placeat) {
notificationPosition = notification.placeat;
} else {
// If a modal is open, we want to make sure the error message is visible and therefore
// show it within the opened modal
const modalSelector = '.modal.open .modal-content';
const modal = document.querySelector(modalSelector);
if (modal) {
if (!modal.querySelector('#modalNotificationContainer')) {
$(modal).prepend('');
}
notificationPosition = `${modalSelector} #modalNotificationContainer`;
addMethod = this.prependNotification;
}
}
const group = notification.group
|| (notificationPosition ? notificationPosition.toString() : '');
this.initializeNotificationContainer(notificationPosition, group);
const notificationInstanceId = (this.nextNotificationId += 1).toString();
addMethod.call(this, {
...notification,
noclear: !!notification.noclear,
group,
notificationId: notification.id,
notificationInstanceId,
type: notification.type || 'transient',
});
return notificationInstanceId;
}
scrollToNotification(notificationInstanceId: string) {
setTimeout(() => {
const element = document.querySelector(
`[data-notification-instance-id='${notificationInstanceId}']`,
) as HTMLElement;
if (element) {
Matomo.helper.lazyScrollTo(element, 250);
}
});
}
/**
* Shows a notification at a certain point with a quick upwards animation.
*/
toast(notification: Notification): void {
this.checkMessage(notification.message);
const $placeat = notification.placeat ? $(notification.placeat) : undefined;
if (!$placeat || !$placeat.length) {
throw new Error('A valid selector is required for the placeat option when using Notification.toast().');
}
const toastElement = document.createElement('div');
toastElement.style.position = 'absolute';
toastElement.style.top = `${$placeat.offset()!.top}px`;
toastElement.style.left = `${$placeat.offset()!.left}px`;
toastElement.style.zIndex = '1000';
document.body.appendChild(toastElement);
const app = createVueApp({
render: () => createVNode(NotificationComponent, {
...notification,
notificationId: notification.id,
type: 'toast',
onClosed: () => {
app.unmount();
},
}),
});
app.mount(toastElement);
}
private initializeNotificationContainer(
notificationPosition: Notification['placeat'],
group: string,
) {
if (!notificationPosition) {
return;
}
const $container = $(notificationPosition);
if ($container.children('.notification-group').length) {
return;
}
// avoiding a dependency cycle. won't need to do this when NotificationGroup's do not need
// to be dynamically initialized.
const NotificationGroup = (window as any).CoreHome.NotificationGroup; // eslint-disable-line
const app = createVueApp({
template: '',
data: () => ({ group }),
});
app.component('NotificationGroup', NotificationGroup);
app.mount($container[0]);
}
private checkMessage(message: string) {
if (!message) {
throw new Error('No message given, cannot display notification');
}
}
}
const instance = new NotificationsStore();
export default instance;
// parse notifications on dom load
$(() => instance.parseNotificationDivs());