UNPKG

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
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