ngx-pwa
Version:
Provides functionality around the progressive web app functionality in angular. Most notably the an approach to cache POST, UPDATE and DELETE requests.
833 lines (813 loc) • 42.5 kB
JavaScript
import { isPlatformBrowser, CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { InjectionToken, Inject, PLATFORM_ID, Component, Input, Injectable } from '@angular/core';
import { __decorate, __param } from 'tslib';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { omit, cloneDeep } from 'lodash';
import * as i3$1 from '@angular/material/badge';
import { MatBadgeModule } from '@angular/material/badge';
import * as i3 from '@angular/material/button';
import { MatButtonModule } from '@angular/material/button';
import * as i1$1 from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import * as i4 from '@angular/material/divider';
import { MatDividerModule } from '@angular/material/divider';
import * as DOMPurify from 'dompurify';
import * as i1 from '@angular/platform-browser';
import { HttpContextToken } from '@angular/common/http';
import { v4 } from 'uuid';
/**
* Encapsulates functionality of lodash.
*/
class LodashUtilities {
/**
* The opposite of `_.pick`; this method creates an object composed of the
* own and inherited enumerable properties of `object` that are not omitted.
* @param object - The source object.
* @param paths - The property names to omit, specified
* individually or in arrays.
* @returns Returns the new object.
*/
static omit(object, ...paths) {
return omit(object, ...paths);
}
/**
* This method is like _.clone except that it recursively clones value.
* @param value - The value to recursively clone.
* @returns Returns the deep cloned value.
*/
static cloneDeep(value) {
return cloneDeep(value);
}
}
/**
* Some common http methods.
*/
var HttpMethod;
(function (HttpMethod) {
HttpMethod["POST"] = "POST";
HttpMethod["GET"] = "GET";
HttpMethod["PATCH"] = "PATCH";
HttpMethod["DELETE"] = "DELETE";
})(HttpMethod || (HttpMethod = {}));
// eslint-disable-next-line jsdoc/require-jsdoc, typescript/typedef
const NGX_PWA_OFFLINE_SERVICE = new InjectionToken('Provider for the OfflineService used eg. in the offline request interceptor.', {
providedIn: 'root',
factory: () => {
// eslint-disable-next-line no-console
console.error(
// eslint-disable-next-line stylistic/max-len
'No OfflineService has been provided for the token NGX_OFFLINE_SERVICE\nAdd this to your app.module.ts provider array:\n{\n provide: NGX_PWA_OFFLINE_SERVICE,\n useExisting: MyOfflineService\n}');
}
});
/**
* The base class for an offline service.
*/
let NgxPwaOfflineService = class NgxPwaOfflineService {
http;
snackBar;
zone;
platformId;
/**
* The key under which any requests are saved in local storage.
*/
CACHED_REQUESTS_KEY = 'requests';
/**
* The prefix of offline generated ids.
* Is used to check if a request still has unresolved dependencies.
*/
OFFLINE_ID_PREFIX = 'offline';
/**
* A snackbar message to display when the synchronization of all cached requests has been finished.
*/
ALL_SYNC_FINISHED_SNACK_BAR_MESSAGE = 'Synchronization finished';
/**
* A snackbar message to display when the synchronization of all cached requests fails.
*/
ALL_SYNC_FAILED_SNACK_BAR_MESSAGE = 'Synchronization failed, please try again later';
/**
* A snackbar message to display when the synchronization of a single cached requests has been finished.
*/
SINGLE_SYNC_FINISHED_SNACK_BAR_MESSAGE = 'Synchronization finished';
/**
* A snackbar message to display when the synchronization of a single cached requests fails.
*/
SINGLE_SYNC_FAILED_SNACK_BAR_MESSAGE = 'Synchronization failed, please try again later';
/**
* Whether or not the user has no internet connection.
*/
isOffline = false;
/**
* A subject of all the requests that have been done while offline.
* Needs to be used for applying offline data or syncing the requests to the api.
*/
cachedRequestsSubject;
// eslint-disable-next-line jsdoc/require-returns
/**
* The currently stored cached requests (if there are any).
*/
get cachedRequests() {
return this.cachedRequestsSubject.value;
}
set cachedRequests(cachedRequests) {
if (!isPlatformBrowser(this.platformId)) {
return;
}
localStorage.setItem(this.CACHED_REQUESTS_KEY, JSON.stringify(cachedRequests));
this.cachedRequestsSubject.next(cachedRequests);
}
constructor(http, snackBar, zone, platformId) {
this.http = http;
this.snackBar = snackBar;
this.zone = zone;
this.platformId = platformId;
if (!isPlatformBrowser(platformId)) {
this.isOffline = false;
this.cachedRequestsSubject = new BehaviorSubject([]);
return;
}
this.isOffline = !navigator.onLine;
window.ononline = () => this.isOffline = !navigator.onLine;
window.onoffline = () => this.isOffline = !navigator.onLine;
const stringData = localStorage.getItem(this.CACHED_REQUESTS_KEY);
const requestsData = stringData ? JSON.parse(stringData) : [];
this.cachedRequestsSubject = new BehaviorSubject(requestsData);
}
/**
* Applies any offline data that has been cached to the given values.
* @param type - The type of the provided entities. Is needed to check if any cached requests of the same type exist.
* @param entities - The already existing data.
* @returns The already existing entities extended/modified by the offline cached requests.
*/
applyOfflineData(type, entities) {
if (!this.cachedRequests.length) {
return entities;
}
const res = Array.from(entities);
const cachedRequests = this.cachedRequests.filter(req => req.metadata.type === type);
for (const req of cachedRequests) {
switch (req.request.method) {
case HttpMethod.POST: {
res.push(req.request.body);
break;
}
case HttpMethod.PATCH: {
const patchIdKey = req.metadata.idKey;
const index = res.findIndex(e => req.request.urlWithParams.includes(`${e[patchIdKey]}`));
res[index] = this.updateOffline(req.request.body, res[index]);
break;
}
case HttpMethod.DELETE: {
const deleteIdKey = req.metadata.idKey;
res.splice(res.findIndex(e => req.request.urlWithParams.includes(`${e[deleteIdKey]}`)), 1);
break;
}
default: {
// eslint-disable-next-line no-console
console.error('There was an unknown http-method in one of your cached offline requests:', req.request.method);
break;
}
}
}
return res;
}
/**
* Applies an UPDATE to an entity without sending a request to the server.
* @param changes - The changes that should be made to the entity.
* @param entity - The entity that should be updated.
* @returns The updated entity.
*/
updateOffline(changes, entity) {
for (const key in changes) {
entity[key] = changes[key];
}
return entity;
}
/**
* Sends a specific cached request to the server.
* @param request - The request that should be synced.
*/
async sync(request) {
const cachedRequestsPriorChanges = LodashUtilities.cloneDeep(this.cachedRequests);
try {
const res = await this.syncSingleRequest(request);
this.zone.run(() => {
this.snackBar.open(this.SINGLE_SYNC_FINISHED_SNACK_BAR_MESSAGE, undefined, { duration: 2500 });
});
this.removeSingleRequest(request);
this.updateOfflineIdsInRequests(request, res);
}
catch {
this.zone.run(() => {
this.snackBar.open(this.SINGLE_SYNC_FAILED_SNACK_BAR_MESSAGE, undefined, { duration: 2500 });
});
this.cachedRequests = cachedRequestsPriorChanges;
}
}
/**
* Sends all cached requests to the server. Tries to handle dependencies of requests on each other.
*/
async syncAll() {
const cachedRequestsPriorChanges = LodashUtilities.cloneDeep(this.cachedRequests);
try {
await this.syncAllRecursive();
this.zone.run(() => {
this.snackBar.open(this.ALL_SYNC_FINISHED_SNACK_BAR_MESSAGE, undefined, { duration: 2500 });
});
this.cachedRequests = [];
}
catch {
this.zone.run(() => {
this.snackBar.open(this.ALL_SYNC_FAILED_SNACK_BAR_MESSAGE, undefined, { duration: 2500 });
});
this.cachedRequests = cachedRequestsPriorChanges;
}
}
/**
* The recursive method used to syn all requests to the api.
*/
async syncAllRecursive() {
// eslint-disable-next-line stylistic/max-len
const request = this.cachedRequests.find(r => !this.hasUnresolvedDependency(r));
if (!request) {
return;
}
const res = await this.syncSingleRequest(request);
this.updateOfflineIdsInRequests(request, res);
await this.syncAllRecursive();
}
/**
* Sends a single cached request to the server.
* @param request - The request that should be synced.
* @returns A promise of the request result.
*/
async syncSingleRequest(request) {
if (this.isOffline || this.hasUnresolvedDependency(request)) {
throw new Error('Could not sync the request');
}
const requestObservable = this.request(request);
if (!requestObservable) {
throw new Error('Could not sync the request');
}
return await firstValueFrom(requestObservable);
}
updateOfflineIdsInRequests(request, res) {
if (this.cachedRequests.length && request.request.body != undefined) {
const idKey = request.metadata.idKey;
if (res[idKey] != undefined) {
const requestsString = `${this.cachedRequests}`.split(request.request.body[idKey]).join(res[idKey]);
this.cachedRequests = JSON.parse(requestsString);
}
}
}
/**
* Calls http.post/patch/delete etc. On the provided request.
* @param request - The request that should be sent.
* @returns The observable of the request or undefined if something went wrong.
*/
request(request) {
switch (request.request.method) {
case HttpMethod.POST: {
return this.http.post(request.request.urlWithParams, LodashUtilities.omit(request.request.body, request.metadata.idKey));
}
case HttpMethod.PATCH: {
return this.http.patch(request.request.urlWithParams, request.request.body);
}
case HttpMethod.DELETE: {
return this.http.delete(request.request.urlWithParams);
}
default: {
return undefined;
}
}
}
/**
* Checks if the given request has an unresolved dependency by looking for the keyword 'offline' inside of it.
* @param request - The request that should be checked.
* @returns Whether or no the given request has an unresolved dependency.
*/
hasUnresolvedDependency(request) {
return request.request.urlWithParams.includes(this.OFFLINE_ID_PREFIX)
|| `${request.request.body}`.includes(this.OFFLINE_ID_PREFIX);
}
/**
* Removes a single request from the cache.
* @param request - The request that should be removed.
*/
removeSingleRequest(request) {
this.cachedRequests.splice(this.cachedRequests.indexOf(request), 1);
this.cachedRequests = this.cachedRequests;
}
};
NgxPwaOfflineService = __decorate([
__param(3, Inject(PLATFORM_ID))
], NgxPwaOfflineService);
/**
* Contains HelperMethods around handling the purification of html strings.
* Is less strict than angular's own sanitizer.
*/
class PurifyUtilities {
/**
* Sanitizes the given source string.
* @param source - The html value as a string.
* @returns A sanitized string of the given source.
*/
static sanitize(source) {
return DOMPurify.sanitize(source);
}
}
/**
* The internal dialog data for the synchronize dialog.
* Sets default values.
*/
class SynchronizeDialogDataInternal {
// eslint-disable-next-line jsdoc/require-jsdoc
title;
// eslint-disable-next-line jsdoc/require-jsdoc
closeButtonLabel;
// eslint-disable-next-line jsdoc/require-jsdoc
syncAllButtonLabel;
// eslint-disable-next-line jsdoc/require-jsdoc
undoAllButtonLabel;
constructor(data) {
this.title = data?.title ?? 'Sync';
this.closeButtonLabel = data?.closeButtonLabel ?? 'Ok';
this.syncAllButtonLabel = data?.syncAllButtonLabel ?? 'Sync all';
this.undoAllButtonLabel = data?.undoAllButtonLabel ?? 'Undo all';
}
}
/**
* The dialog for syncing cached requests to the server.
*/
class NgxPwaSynchronizeDialogComponent {
offlineService;
sanitizer;
dialogRef;
data;
// eslint-disable-next-line jsdoc/require-jsdoc
PurifyUtilities = PurifyUtilities;
/**
* The provided dialog data filled up with default values.
*/
dialogData;
constructor(offlineService, sanitizer, dialogRef, data) {
this.offlineService = offlineService;
this.sanitizer = sanitizer;
this.dialogRef = dialogRef;
this.data = data;
}
ngOnInit() {
this.dialogData = new SynchronizeDialogDataInternal(this.data);
}
/**
* Sends a specific cached request to the server.
* @param request - The request that should be synced.
*/
async syncSingleRequest(request) {
await this.offlineService.sync(request);
if (!this.offlineService.cachedRequests.length) {
this.dialogRef.close();
}
}
/**
* Removes a single request from the cache.
* @param request - The request that should be removed.
*/
removeSingleRequest(request) {
this.offlineService.removeSingleRequest(request);
if (!this.offlineService.cachedRequests.length) {
this.dialogRef.close();
}
}
/**
* Sends all cached requests to the server. Tries to handle dependencies of requests on each other.
*/
async syncAll() {
await this.offlineService.syncAll();
if (!this.offlineService.cachedRequests.length) {
this.dialogRef.close();
}
}
/**
* Removes all locally cached requests.
*/
undoAll() {
this.offlineService.cachedRequests = [];
this.dialogRef.close();
}
/**
* Closes the dialog.
*/
close() {
this.dialogRef.close();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxPwaSynchronizeDialogComponent, deps: [{ token: NGX_PWA_OFFLINE_SERVICE }, { token: i1.DomSanitizer }, { token: i1$1.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: NgxPwaSynchronizeDialogComponent, isStandalone: true, selector: "ngx-pwa-synchronize-dialog", ngImport: i0, template: "<h1 mat-dialog-title>{{dialogData.title}}</h1>\n<div mat-dialog-content>\n <div class=\"all-button-row\">\n <button type=\"button\" mat-raised-button [disabled]=\"offlineService.isOffline\" (click)=\"syncAll()\">\n {{dialogData.syncAllButtonLabel}}\n </button>\n <button type=\"button\" mat-raised-button (click)=\"undoAll()\">\n {{dialogData.undoAllButtonLabel}}\n </button>\n </div>\n <div class=\"mat-elevation-z4 request-box\">\n @for (request of offlineService.cachedRequests; track $index) {\n <div class=\"request-item\">\n <!-- eslint-disable-next-line angular/no-call-expression -->\n <div [innerHtml]=\"sanitizer.bypassSecurityTrustHtml(PurifyUtilities.sanitize(request.metadata.displayValue))\">\n </div>\n <span>\n <!-- eslint-disable-next-line angular/no-call-expression -->\n <button type=\"button\" mat-icon-button [disabled]=\"offlineService.isOffline || offlineService.hasUnresolvedDependency(request)\" (click)=\"syncSingleRequest(request)\">\n <i class=\"fas fa-upload\"></i>\n </button>\n <button type=\"button\" mat-icon-button color=\"warn\" (click)=\"removeSingleRequest(request)\">\n <i class=\"fas fa-trash\"></i>\n </button>\n </span>\n </div>\n <mat-divider></mat-divider>\n }\n </div>\n</div>\n<div mat-dialog-actions>\n <button type=\"button\" mat-raised-button class=\"cancel-button\" (click)=\"close()\">{{dialogData.closeButtonLabel}}</button>\n</div>", styles: ["h1{text-align:center}.all-button-row{margin-top:10px;margin-bottom:15px;display:flex;flex-wrap:nowrap;gap:10px}.all-button-row button{flex:50%}.request-box{border-radius:5px;margin-bottom:10px}.request-box .request-item{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;padding:15px;line-break:anywhere}.request-box i{font-size:22px;line-height:22px}.cancel-button{margin-left:auto;margin-right:auto}@media (max-width: 1200px){.all-button-row{flex-wrap:wrap}.all-button-row button{flex:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i4.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1$1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1$1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1$1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxPwaSynchronizeDialogComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-pwa-synchronize-dialog', standalone: true, imports: [
CommonModule,
MatButtonModule,
MatDividerModule,
MatDialogModule
], template: "<h1 mat-dialog-title>{{dialogData.title}}</h1>\n<div mat-dialog-content>\n <div class=\"all-button-row\">\n <button type=\"button\" mat-raised-button [disabled]=\"offlineService.isOffline\" (click)=\"syncAll()\">\n {{dialogData.syncAllButtonLabel}}\n </button>\n <button type=\"button\" mat-raised-button (click)=\"undoAll()\">\n {{dialogData.undoAllButtonLabel}}\n </button>\n </div>\n <div class=\"mat-elevation-z4 request-box\">\n @for (request of offlineService.cachedRequests; track $index) {\n <div class=\"request-item\">\n <!-- eslint-disable-next-line angular/no-call-expression -->\n <div [innerHtml]=\"sanitizer.bypassSecurityTrustHtml(PurifyUtilities.sanitize(request.metadata.displayValue))\">\n </div>\n <span>\n <!-- eslint-disable-next-line angular/no-call-expression -->\n <button type=\"button\" mat-icon-button [disabled]=\"offlineService.isOffline || offlineService.hasUnresolvedDependency(request)\" (click)=\"syncSingleRequest(request)\">\n <i class=\"fas fa-upload\"></i>\n </button>\n <button type=\"button\" mat-icon-button color=\"warn\" (click)=\"removeSingleRequest(request)\">\n <i class=\"fas fa-trash\"></i>\n </button>\n </span>\n </div>\n <mat-divider></mat-divider>\n }\n </div>\n</div>\n<div mat-dialog-actions>\n <button type=\"button\" mat-raised-button class=\"cancel-button\" (click)=\"close()\">{{dialogData.closeButtonLabel}}</button>\n</div>", styles: ["h1{text-align:center}.all-button-row{margin-top:10px;margin-bottom:15px;display:flex;flex-wrap:nowrap;gap:10px}.all-button-row button{flex:50%}.request-box{border-radius:5px;margin-bottom:10px}.request-box .request-item{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;padding:15px;line-break:anywhere}.request-box i{font-size:22px;line-height:22px}.cancel-button{margin-left:auto;margin-right:auto}@media (max-width: 1200px){.all-button-row{flex-wrap:wrap}.all-button-row button{flex:100%}}\n"] }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [NGX_PWA_OFFLINE_SERVICE]
}] }, { type: i1.DomSanitizer }, { type: i1$1.MatDialogRef }, { type: undefined, decorators: [{
type: Inject,
args: [MAT_DIALOG_DATA]
}] }] });
/**
* Displays a badge with the amount of cached offline request.
* Can be clicked to open a dialog to sync cached requests to the server.
*/
class NgxPwaSynchronizeBadgeComponent {
offlineService;
dialog;
/**
* Configuration data for the Synchronize Dialog.
*/
synchronizeDialogData;
constructor(offlineService, dialog) {
this.offlineService = offlineService;
this.dialog = dialog;
}
/**
* Opens the dialog for syncing cached requests to the server.
*/
openSyncDialog() {
this.dialog.open(NgxPwaSynchronizeDialogComponent, {
autoFocus: false,
restoreFocus: false,
minWidth: '40%',
data: this.synchronizeDialogData
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxPwaSynchronizeBadgeComponent, deps: [{ token: NGX_PWA_OFFLINE_SERVICE }, { token: i1$1.MatDialog }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: NgxPwaSynchronizeBadgeComponent, isStandalone: true, selector: "ngx-pwa-synchronize-badge", inputs: { synchronizeDialogData: "synchronizeDialogData" }, ngImport: i0, template: "@if (offlineService.cachedRequests.length) {\n <button type=\"button\" mat-button (click)=\"openSyncDialog()\">\n <i class=\"fas fa-rotate\" matBadgePosition=\"above after\" matBadgeColor=\"warn\" [matBadge]=\"offlineService.cachedRequests.length\"></i>\n </button>\n}", styles: ["i{font-size:22px;height:24px}i:before{display:inline-block;margin-top:2px;text-align:end;transform:scaleX(-1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatBadgeModule }, { kind: "directive", type: i3$1.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "ngmodule", type: MatDialogModule }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxPwaSynchronizeBadgeComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-pwa-synchronize-badge', standalone: true, imports: [
CommonModule,
MatButtonModule,
MatBadgeModule,
MatDialogModule
], template: "@if (offlineService.cachedRequests.length) {\n <button type=\"button\" mat-button (click)=\"openSyncDialog()\">\n <i class=\"fas fa-rotate\" matBadgePosition=\"above after\" matBadgeColor=\"warn\" [matBadge]=\"offlineService.cachedRequests.length\"></i>\n </button>\n}", styles: ["i{font-size:22px;height:24px}i:before{display:inline-block;margin-top:2px;text-align:end;transform:scaleX(-1)}\n"] }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [NGX_PWA_OFFLINE_SERVICE]
}] }, { type: i1$1.MatDialog }], propDecorators: { synchronizeDialogData: [{
type: Input
}] } });
/**
* Shows a offline warning when the user is not online.
*/
class NgxPwaOfflineStatusBarComponent {
offlineService;
/**
* The message to display when the user is offline.
* @default 'Offline'
*/
offlineMessage;
/**
* The message to display when the user has changes that aren't synced to the api.
* @default 'Unsaved Changes'
*/
unsavedChangesMessage;
/**
* Whether or not to display a badge that shows the amount of cached requests and can open a dialog to sync changes to the server.
*/
displayUnsavedChangesSynchronizeBadge;
/**
* Configuration data for the Synchronize Dialog.
*/
synchronizeDialogData;
constructor(offlineService) {
this.offlineService = offlineService;
}
ngOnInit() {
this.offlineMessage = this.offlineMessage ?? 'Offline';
this.unsavedChangesMessage = this.unsavedChangesMessage ?? 'Unsaved Changes';
this.displayUnsavedChangesSynchronizeBadge = this.displayUnsavedChangesSynchronizeBadge ?? true;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxPwaOfflineStatusBarComponent, deps: [{ token: NGX_PWA_OFFLINE_SERVICE }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: NgxPwaOfflineStatusBarComponent, isStandalone: true, selector: "ngx-pwa-offline-status-bar", inputs: { offlineMessage: "offlineMessage", unsavedChangesMessage: "unsavedChangesMessage", displayUnsavedChangesSynchronizeBadge: "displayUnsavedChangesSynchronizeBadge", synchronizeDialogData: "synchronizeDialogData" }, ngImport: i0, template: "@if (offlineService.isOffline) {\n <div>\n {{offlineMessage}}\n @if (displayUnsavedChangesSynchronizeBadge) {\n <ngx-pwa-synchronize-badge [synchronizeDialogData]=\"synchronizeDialogData\"></ngx-pwa-synchronize-badge>\n }\n </div>\n}\n@else if (offlineService.cachedRequests.length) {\n <div>\n {{unsavedChangesMessage}}\n @if (displayUnsavedChangesSynchronizeBadge) {\n <ngx-pwa-synchronize-badge [synchronizeDialogData]=\"synchronizeDialogData\"></ngx-pwa-synchronize-badge>\n }\n </div>\n}", styles: ["div{background-color:orange;font-weight:bolder;letter-spacing:1px;text-transform:uppercase;height:50px;display:flex;justify-content:center;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: NgxPwaSynchronizeBadgeComponent, selector: "ngx-pwa-synchronize-badge", inputs: ["synchronizeDialogData"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxPwaOfflineStatusBarComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-pwa-offline-status-bar', standalone: true, imports: [
CommonModule,
NgxPwaSynchronizeBadgeComponent
], template: "@if (offlineService.isOffline) {\n <div>\n {{offlineMessage}}\n @if (displayUnsavedChangesSynchronizeBadge) {\n <ngx-pwa-synchronize-badge [synchronizeDialogData]=\"synchronizeDialogData\"></ngx-pwa-synchronize-badge>\n }\n </div>\n}\n@else if (offlineService.cachedRequests.length) {\n <div>\n {{unsavedChangesMessage}}\n @if (displayUnsavedChangesSynchronizeBadge) {\n <ngx-pwa-synchronize-badge [synchronizeDialogData]=\"synchronizeDialogData\"></ngx-pwa-synchronize-badge>\n }\n </div>\n}", styles: ["div{background-color:orange;font-weight:bolder;letter-spacing:1px;text-transform:uppercase;height:50px;display:flex;justify-content:center;align-items:center}\n"] }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [NGX_PWA_OFFLINE_SERVICE]
}] }], propDecorators: { offlineMessage: [{
type: Input
}], unsavedChangesMessage: [{
type: Input
}], displayUnsavedChangesSynchronizeBadge: [{
type: Input
}], synchronizeDialogData: [{
type: Input
}] } });
/**
* The internal data to customize the Version Ready Dialog.
* Sets default values.
*/
class VersionReadyDialogDataInternal {
// eslint-disable-next-line jsdoc/require-jsdoc
title;
// eslint-disable-next-line jsdoc/require-jsdoc
message;
// eslint-disable-next-line jsdoc/require-jsdoc
confirmButtonLabel;
// eslint-disable-next-line jsdoc/require-jsdoc
cancelButtonLabel;
constructor(data) {
this.title = data?.title ?? 'New Update available';
this.message = data?.message ?? 'A new version has been downloaded. Do you want to install it now?';
this.confirmButtonLabel = data?.confirmButtonLabel ?? 'Reload';
this.cancelButtonLabel = data?.cancelButtonLabel ?? 'Not now';
}
}
/**
* A dialog that gets displayed when a new version of the pwa has been downloaded and is ready for install.
*/
class NgxPwaVersionReadyDialogComponent {
dialogRef;
data;
/**
* The data to customize the Version Ready Dialog.
* Is built from the MAT_DIALOG_DATA input.
*/
versionReadyDialogData;
constructor(dialogRef, data) {
this.dialogRef = dialogRef;
this.data = data;
}
ngOnInit() {
this.versionReadyDialogData = new VersionReadyDialogDataInternal(this.data);
}
/**
* Closes the dialog with data to trigger a reload of the app.
*/
update() {
this.dialogRef.close('update');
}
/**
* Closes the dialog with data to not trigger anything.
*/
cancel() {
this.dialogRef.close('cancel');
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxPwaVersionReadyDialogComponent, deps: [{ token: i1$1.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: NgxPwaVersionReadyDialogComponent, isStandalone: true, selector: "ngx-pwa-version-ready-dialog", ngImport: i0, template: "<h1 mat-dialog-title>{{versionReadyDialogData.title}}</h1>\n<mat-dialog-content>\n {{versionReadyDialogData.message}}\n</mat-dialog-content>\n<mat-dialog-actions>\n <button type=\"button\" mat-raised-button (click)=\"update()\">{{versionReadyDialogData.confirmButtonLabel}}</button>\n <button type=\"button\" mat-raised-button (click)=\"cancel()\">{{versionReadyDialogData.cancelButtonLabel}}</button>\n</mat-dialog-actions>", styles: ["mat-dialog-actions{display:flex;flex-wrap:nowrap;gap:10px;padding-left:24px;padding-right:24px}mat-dialog-actions button{flex:50%}@media (max-width: 1200px){mat-dialog-actions{flex-wrap:wrap}mat-dialog-actions button{flex:100%}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1$1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1$1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1$1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxPwaVersionReadyDialogComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-pwa-version-ready-dialog', standalone: true, imports: [
MatButtonModule,
MatDialogModule
], template: "<h1 mat-dialog-title>{{versionReadyDialogData.title}}</h1>\n<mat-dialog-content>\n {{versionReadyDialogData.message}}\n</mat-dialog-content>\n<mat-dialog-actions>\n <button type=\"button\" mat-raised-button (click)=\"update()\">{{versionReadyDialogData.confirmButtonLabel}}</button>\n <button type=\"button\" mat-raised-button (click)=\"cancel()\">{{versionReadyDialogData.cancelButtonLabel}}</button>\n</mat-dialog-actions>", styles: ["mat-dialog-actions{display:flex;flex-wrap:nowrap;gap:10px;padding-left:24px;padding-right:24px}mat-dialog-actions button{flex:50%}@media (max-width: 1200px){mat-dialog-actions{flex-wrap:wrap}mat-dialog-actions button{flex:100%}}\n"] }]
}], ctorParameters: () => [{ type: i1$1.MatDialogRef }, { type: undefined, decorators: [{
type: Inject,
args: [MAT_DIALOG_DATA]
}] }] });
// eslint-disable-next-line stylistic/max-len, jsdoc/require-jsdoc
const NGX_PWA_HTTP_CONTEXT_METADATA = new HttpContextToken(() => undefined);
/**
* A base service that provides functionality regarding notifications.
*/
class NgxPwaNotificationService {
swPush;
http;
// eslint-disable-next-line jsdoc/require-returns
/**
* Whether or not the current user has notifications enabled.
*/
get hasNotificationsEnabled() {
return this.swPush.isEnabled;
}
constructor(swPush, http) {
this.swPush = swPush;
this.http = http;
}
/**
* Asks the user for permission to use push notifications.
*/
async askForNotificationPermission() {
const pushSubscription = await this.swPush.requestSubscription({ serverPublicKey: this.VAPID_PUBLIC_KEY });
void this.enableNotifications(pushSubscription);
}
/**
* Enables notifications by sending a push subscription to the server.
* @param pushSubscription - The push subscription to send to the server.
*/
async enableNotifications(pushSubscription) {
await firstValueFrom(this.http.post(this.API_ENABLE_NOTIFICATIONS_URL, pushSubscription));
}
/**
* Disables notifications.
*/
async disableNotifications() {
const pushSubscription = await firstValueFrom(this.swPush.subscription);
if (pushSubscription == undefined) {
return;
}
await firstValueFrom(this.http.post(this.API_DISABLE_NOTIFICATIONS_URL, pushSubscription));
await this.swPush.unsubscribe();
}
}
/**
* Encapsulates functionality of uuid.
*/
class UuidUtilities {
/**
* Generates a uuid.
* @returns A random new uuid.
*/
static generate() {
return v4();
}
}
/**
* The internal request metadata.
* Sets default values.
*/
class RequestMetadataInternal {
idKey;
type;
displayValue;
constructor(request, data) {
this.idKey = data?.idKey ?? 'id';
this.type = data?.type ?? '';
this.displayValue = data?.displayValue ?? defaultCachedRequestMetadata(request);
}
}
function defaultCachedRequestMetadata(req) {
const color = getColorForHttpMethod(req.method);
return `<b style="color: ${color};">${req.method}</b> ${req.url}`;
}
function getColorForHttpMethod(method) {
switch (method) {
case HttpMethod.POST: {
return 'green';
}
case HttpMethod.PATCH: {
return 'black';
}
case HttpMethod.DELETE: {
return 'red';
}
default: {
return 'black';
}
}
}
/**
* An interceptor that caches any POST, UPDATE or DELETE requests when the user is offline.
*/
class OfflineRequestInterceptor {
offlineService;
constructor(offlineService) {
this.offlineService = offlineService;
}
// eslint-disable-next-line jsdoc/require-jsdoc
intercept(req, next) {
if (!this.requestShouldBeCached(req)) {
return next.handle(req);
}
const metadata = this.getRequestMetadata(req);
if (req.method === HttpMethod.POST && req.body != undefined) {
req.body[metadata.idKey] = `${this.offlineService.OFFLINE_ID_PREFIX} ${UuidUtilities.generate()}`;
}
const cachedRequest = {
request: req,
metadata: metadata
};
this.offlineService.cachedRequests = this.offlineService.cachedRequests.concat(cachedRequest);
return next.handle(req);
}
getRequestMetadata(request) {
const metadata = request.context.get(NGX_PWA_HTTP_CONTEXT_METADATA);
if (!metadata) {
// eslint-disable-next-line no-console
console.error('No metadata for the request', request.urlWithParams, ' was found.\nUsing fallback default values.');
}
const internalMetadata = new RequestMetadataInternal(request, metadata);
return internalMetadata;
}
requestShouldBeCached(req) {
return this.offlineService.isOffline
&& this.requestMethodIsPostPatchOrDelete(req)
&& !this.urlShouldNotBeCached(req.url);
}
requestMethodIsPostPatchOrDelete(req) {
return req.method === HttpMethod.POST || req.method === HttpMethod.PATCH || req.method === HttpMethod.DELETE;
}
urlShouldNotBeCached(url) {
return url.endsWith('/login')
|| url.endsWith('/register')
|| url.endsWith('/refresh-token')
|| url.endsWith('/request-reset-password')
|| url.endsWith('/confirm-reset-password')
|| url.endsWith('/verify-password-reset-token');
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OfflineRequestInterceptor, deps: [{ token: NGX_PWA_OFFLINE_SERVICE }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OfflineRequestInterceptor });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OfflineRequestInterceptor, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [NGX_PWA_OFFLINE_SERVICE]
}] }] });
/**
* Provides helpers for handling pwa version updates.
*/
class NgxPwaUpdateService {
swUpdate;
dialog;
constructor(swUpdate, dialog) {
this.swUpdate = swUpdate;
this.dialog = dialog;
}
/**
* Subscribes to any version update events.
*/
subscribeToUpdateEvents() {
if (!this.swUpdate.isEnabled) {
return;
}
this.swUpdate.versionUpdates.subscribe(e => {
switch (e.type) {
case 'VERSION_READY': {
void this.onVersionReady();
break;
}
case 'VERSION_DETECTED': {
this.onVersionDetected();
break;
}
case 'VERSION_INSTALLATION_FAILED': {
this.onVersionInstallationFailed();
break;
}
case 'NO_NEW_VERSION_DETECTED': {
this.onNoNewVersionDetected();
break;
}
}
});
}
/**
* Gets called when no new version was found.
*/
onNoNewVersionDetected() {
return;
}
/**
* Gets called when the installation of a new version fails.
*/
onVersionInstallationFailed() {
return;
}
/**
* Gets called when a new version has been found.
*/
onVersionDetected() {
return;
}
/**
* Gets called when a new version has been installed.
*/
async onVersionReady() {
const dialogRef = this.dialog.open(NgxPwaVersionReadyDialogComponent, {
autoFocus: false,
restoreFocus: false
});
const res = await firstValueFrom(dialogRef.afterClosed());
if (res === 'update') {
window.location.reload();
}
}
/**
* Manually checks for updates.
* @returns Whether or not new updates are available.
*/
async checkForUpdates() {
return await this.swUpdate.checkForUpdate();
}
}
/*
* Public API Surface of ngx-pwa
*/
/**
* Generated bundle index. Do not edit.
*/
export { HttpMethod, NGX_PWA_HTTP_CONTEXT_METADATA, NGX_PWA_OFFLINE_SERVICE, NgxPwaNotificationService, NgxPwaOfflineService, NgxPwaOfflineStatusBarComponent, NgxPwaSynchronizeBadgeComponent, NgxPwaSynchronizeDialogComponent, NgxPwaUpdateService, NgxPwaVersionReadyDialogComponent, OfflineRequestInterceptor };
//# sourceMappingURL=ngx-pwa.mjs.map