UNPKG

@studiohyperdrive/ngx-utils

Version:

A series of abstracts, utils, pipes and services for Angular applications.

1,112 lines (1,088 loc) 69.4 kB
import * as i0 from '@angular/core'; import { EventEmitter, Directive, Input, Output, HostBinding, HostListener, inject, PLATFORM_ID, Injectable, Inject, Pipe, InjectionToken, SecurityContext } from '@angular/core'; import * as i1$2 from '@angular/router'; import { ActivatedRoute } from '@angular/router'; import { isPlatformBrowser, DOCUMENT } from '@angular/common'; import { BehaviorSubject, EMPTY, fromEvent, Subject, NEVER, ReplaySubject, filter, map, tap, takeUntil, take } from 'rxjs'; import * as i1 from '@studiohyperdrive/ngx-core'; import { isObject, uniqBy } from 'lodash'; import clean from 'obj-clean'; import { normalizeString } from '@studiohyperdrive/utils'; import * as i1$1 from '@angular/platform-browser'; class FocusClickDirective { constructor() { // Allow the button to ignore click events when set to true this.disabled = false; // Allow the function passed by the host to be executed // when the emit() method gets called /** * This directive replaces the default `click` directive and allows the user to execute * the `click` event by clicking the mouse **and** by using the `enter` key on focus. * * A tabindex of `0` gets added to the host. * * @memberof FocusClickDirective */ this.focusClick = new EventEmitter(); // Make every tag that uses this directive by default tabbable this.tabIndex = 0; } // Add eventhandler to the click event isClicked(event) { if (!this.disabled) { this.focusClick.emit(event); } } // Add eventhandler to keydown event When enter is pressed and the event // isn't blocked, execute the click function of the host isEntered() { if (!this.disabled) { this.focusClick.emit(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: FocusClickDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.3", type: FocusClickDirective, isStandalone: true, selector: "[focusClick]", inputs: { disabled: "disabled" }, outputs: { focusClick: "focusClick" }, host: { listeners: { "click": "isClicked($event)", "keydown.enter": "isEntered()" }, properties: { "attr.tabindex": "this.tabIndex" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: FocusClickDirective, decorators: [{ type: Directive, args: [{ selector: '[focusClick]', standalone: true, }] }], propDecorators: { disabled: [{ type: Input }], focusClick: [{ type: Output }], tabIndex: [{ type: HostBinding, args: ['attr.tabindex'] }], isClicked: [{ type: HostListener, args: ['click', ['$event']] }], isEntered: [{ type: HostListener, args: ['keydown.enter'] }] } }); const Directives = [FocusClickDirective]; const getQueryParams = () => { return inject(ActivatedRoute).queryParams; }; // @dynamic /** * @deprecated: This service has been deprecated in favor of the one in @studiohyperdrive/ngx-core */ class WindowService { /* eslint-enable */ constructor(document, // tslint:disable-next-line: ban-types platformId) { this.document = document; this.platformId = platformId; /* eslint-disable @typescript-eslint/member-ordering */ this.widthSubject$ = new BehaviorSubject(1200); this.scrollingUpSubject$ = new BehaviorSubject(true); this.currentScrollPositionSubject$ = new BehaviorSubject(0); /** * Observable to get the window-width, defaults to 1200 when no window is defined */ this.width$ = this.widthSubject$.asObservable(); /** * Observable to track when the scroll has ended */ this.scrollingUp$ = this.scrollingUpSubject$.asObservable(); /** * Observable of the current scroll position after the scroll has ended */ this.currentScrollPosition$ = this.currentScrollPositionSubject$.asObservable(); /** * Current scroll position after the scroll has ended */ this.currentScrollPosition = 0; if (this.isBrowser() && this.hasDocument()) { this.window = this.document.defaultView; this.document.addEventListener('scroll', this.handleContentScroll.bind(this)); this.widthSubject$.next(this.window.innerWidth); this.window.addEventListener('resize', () => { if (this.window.innerWidth && this.widthSubject$.getValue()) { this.widthSubject$.next(this.window.innerWidth); } }); } } /** * Scrolls to the provided position of the page * * @param offset - Offset to which we want to scroll, scrolls to top when no offset is provided */ scrollTo(offset = 0) { if (!this.window) { return; } this.window.scrollTo(0, offset); } /** * Returns whether there is a document present */ hasDocument() { return !!this.document; } /** * Returns whether the current platform is a browser */ isBrowser() { return isPlatformBrowser(this.platformId); } /** * Run a provided function only when we're in the browser and not in a server side rendered application * * @param action - Function we want to run in the browser */ runInBrowser(action) { if (this.isBrowser) { action({ browserWindow: this.window, browserDocument: this.document }); } else { console.warn('Browser depended function has not run.'); } } handleContentScroll() { if (window.pageYOffset > this.currentScrollPosition) { this.scrollingUpSubject$.next(false); } else { this.scrollingUpSubject$.next(true); } this.currentScrollPosition = window.pageYOffset < 0 ? 0 : window.pageYOffset; this.currentScrollPositionSubject$.next(this.currentScrollPosition); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: WindowService, deps: [{ token: DOCUMENT }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: WindowService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: WindowService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: Document, decorators: [{ type: Inject, args: [DOCUMENT] }] }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }] }); /** * A service that wraps the BroadCastChannel API and provides an Observable based implementation to the channel messages. * * For more information: * https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel */ class NgxBroadcastChannelService { constructor(windowService) { this.windowService = windowService; /** * A record holding all the broadcast channels */ this.broadcastChannel = {}; } /** * initChannel * * The initChannel method initializes a new BroadcastChannel instance. * * @param args{ConstructorParameters<typeof BroadcastChannel>} - The arguments to pass to the BroadcastChannel constructor. */ initChannel(...args) { // Iben: Only run when in browser this.windowService.runInBrowser(() => { const [channelName] = args; if (!channelName) { console.error('NgxUtils: There was an attempt to initialize a BroadcastChannel without providing a name.'); return; } if (!this.broadcastChannel[channelName]) { this.broadcastChannel[channelName] = new BroadcastChannel(...args); } }); } /** * closeChannel * * The closeChannel method closes a selected BroadcastChannel instance. * * @param channelName{string} - The name of the Broadcast Channel. */ closeChannel(channelName) { if (!channelName || !this.broadcastChannel[channelName]) { return; } this.broadcastChannel[channelName].close(); delete this.broadcastChannel[channelName]; } /** * postMessage * * The postMessage method sends a message to a selected BroadcastChannel instance. * * @param channelName{string} - The name of the Broadcast Channel. * @param message{any} - The payload to send through the channel. */ postMessage(channelName, message) { if (!channelName || !this.broadcastChannel[channelName]) { console.error('NgxUtils: There was an attempt to post a message to a channel without providing a name or the selected channel does not exist. The included message was:', message); return; } this.broadcastChannel[channelName].postMessage(message); } /** * selectChannelMessages * * The selectChannelMessages method subscribes to the `message` (bc.onmessage) event of a selected BroadcastChannel instance. * * @param channelName{string} - The name of the Broadcast Channel. * @returns Observable<MessageEvent> - The message event of the channel wrapped in an observable. */ selectChannelMessages(channelName) { if (!channelName || !this.broadcastChannel[channelName]) { console.error("NgxUtils: There was an attempt to select a BroadcastChannel's messages without providing a name or the selected channel does not exist."); return EMPTY; } return fromEvent(this.broadcastChannel[channelName], 'message'); } /** * selectChannelMessageErrors * * The selectChannelMessageErrors method subscribes to the `messageerror` (bc.onmessageerror) event of a selected BroadcastChannel instance. * * @param channelName{string} - The name of the Broadcast Channel. * @returns Observable<MessageEvent> - The messageerror event of the channel wrapped in an observable. */ selectChannelMessageErrors(channelName) { if (!channelName || !this.broadcastChannel[channelName]) { console.error("NgxUtils: There was an attempt to select a BroadcastChannel's message errors without providing a name or the selected channel does not exist."); return EMPTY; } return fromEvent(this.broadcastChannel[channelName], 'messageerror'); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxBroadcastChannelService, deps: [{ token: i1.NgxWindowService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxBroadcastChannelService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxBroadcastChannelService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: i1.NgxWindowService }] }); class SubscriptionService { constructor() { this.destroyed$ = new Subject(); } ngOnDestroy() { this.destroyed$.next(true); this.destroyed$.complete(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: SubscriptionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: SubscriptionService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: SubscriptionService, decorators: [{ type: Injectable }] }); /** * A service that provides a SSR-proof Observable based approach to the session- and localStorage. */ class NgxStorageService { constructor(windowService) { this.windowService = windowService; /** * A record to hold the properties in the sessionStorage */ this.sessionStorageRecord = {}; /** * A record to hold the properties in the localStorage */ this.localStorageRecord = {}; /** * A subject to hold the events of the storage */ this.storageEventSubject = new Subject(); /** * An observable that emits whenever the session- or the localStorage was updated */ this.storageEvents$ = this.storageEventSubject.asObservable(); // Iben: Get the initial values of the session and the local storage windowService.runInBrowser(() => { this.setupStorage(sessionStorage, this.sessionStorageRecord); this.setupStorage(localStorage, this.localStorageRecord); }); } /** * A localStorage implementation using observables */ get localStorage() { return { getItem: (key) => this.getItem(key, localStorage), getItemObservable: (key) => this.getItemObservable(key, this.localStorageRecord), removeItem: (key) => this.removeItem(key, localStorage, this.localStorageRecord, 'local'), setItem: (key, item) => this.setItem(key, item, localStorage, this.localStorageRecord, 'local'), clear: () => this.clearStorage(localStorage, this.localStorageRecord, 'local'), }; } /** * A sessionStorage implementation using observables */ get sessionStorage() { return { getItem: (key) => this.getItem(key, sessionStorage), getItemObservable: (key) => this.getItemObservable(key, this.sessionStorageRecord), removeItem: (key) => this.removeItem(key, sessionStorage, this.sessionStorageRecord, 'session'), setItem: (key, item) => this.setItem(key, item, sessionStorage, this.sessionStorageRecord, 'session'), clear: () => this.clearStorage(sessionStorage, this.sessionStorageRecord, 'session'), }; } getItem(key, storage) { return this.parseValue(storage.getItem(key)); } /** * Returns an observable version of the storage value * * @param key - The key of the storage value * @param record - The storage record */ getItemObservable(key, record) { // Iben: Return NEVER when not in browser if (!this.windowService.isBrowser()) { return NEVER; } // Iben: If the subject already exists, we return the observable if (record[key]) { return record[key].asObservable(); } // Iben: If no subject exits, we create a new one record[key] = new BehaviorSubject(undefined); // Iben: Return the observable return this.getItemObservable(key, record); } /** * Sets an item in the storage * * @param key - The key of the item * @param item - The item in the storage * @param storage - The storage in which we want to save the item * @param record - The corresponding storage record */ setItem(key, item, storage, record, type) { // Iben: Early exit when we're in the browser if (!this.windowService.isBrowser()) { return undefined; } // Iben: Check if there's already a subject for this item. If not, we create one let subject = record[key]; if (!subject) { subject = new BehaviorSubject(undefined); storage[key] = subject; } // Iben: Store the current value of the subject const oldValue = subject.getValue(); // Iben: Set the item in the storage storage.setItem(key, typeof item === 'string' ? item : JSON.stringify(item)); // Iben: Update the subject in the record subject.next(item); // Iben: Create the storage event const event = { key, newValue: item, oldValue, storage: type, type: 'set', }; // Iben: Emit the storage event this.storageEventSubject.next(event); // Iben: Return the storage event return event; } /** * Remove an item from the storage and emit a remove event * * @param key - The key of the item * @param storage - The storage we wish to remove the item from * @param record - The record with the subject * @param type - The type of storage */ removeItem(key, storage, record, type) { // Iben: Early exit when we're not in the browser if (!this.windowService.isBrowser()) { return undefined; } // Iben: Get the old item const oldValue = this.parseValue(storage.getItem(key)); // Iben: Remove the item from the storage storage.removeItem(key); // Iben Update the subject if it exists record[key]?.next(undefined); // Iben: Create the event and return and emit it const event = { oldValue, storage: type, key, type: 'remove', }; this.storageEventSubject.next(event); return event; } /** * Clears the storage, completes all subjects and emits a clear event * * @param storage - The storage we wish to clear * @param record - The record with the subjects * @param type - The type of storage */ clearStorage(storage, record, type) { // Iben: Early exit when we're not in the browser if (!this.windowService.isBrowser()) { return undefined; } // Iben: Clear the storage storage.clear(); // Iben: Clear the record and complete all subjects Object.entries(record).forEach(([key, subject]) => { subject.next(undefined); subject.complete(); record[key] = undefined; }); // Iben: Create and emit event const event = { type: 'clear', storage: type, }; this.storageEventSubject.next(event); return event; } /** * Grabs the existing storage and updates the record * * @private * @param {Storage} storage - The current state of the storage * @param {NgxStorageRecord} record * @memberof NgxStorageService */ setupStorage(storage, record) { Object.entries(storage).forEach(([key, value]) => { record[key] = new BehaviorSubject(this.parseValue(value)); }); } /** * Parses a string value from the storage to an actual value * * @param value - The provided string value */ parseValue(value) { // Iben: If the value does not exist, return the value if (!value) { return value; } // Iben: If the value is either true or false, return a boolean version of the value if (value === 'true' || value === 'false') { return value === 'true'; } // Iben: If the value is a number, return the parsed number if (value.match(/^[0-9]*[,.]{0,1}[0-9]*$/)) { return Number(value); } // Iben: If the value is an object, return the parsed object if (value.match(/{(.*:.*[,]{0,1})*}/)) { return JSON.parse(value); } // Iben: Return the string value as is return value; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxStorageService, deps: [{ token: i1.NgxWindowService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxStorageService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxStorageService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.NgxWindowService }] }); /** * A service that can be used to track media queries and their changes. It exposes a method * to register media queries, which takes an array of tuples with the id of the media query * and the query itself. The service will then emit the id of the media query that has * changed when subscribed to the `getMatchingQuery$` method. */ class NgxMediaQueryService { constructor(windowService) { this.windowService = windowService; /** * A map of media queries that are registered with the service. */ this.queryListMap = new Map(); /** * A map of the registered media queries with their id. */ this.queryIdMap = new Map(); /* * A map of listeners that are registered with the service. * They are saved to be able to remove them when the service is destroyed. */ this.mediaQueryListenerMap = new Map(); /** * A subject that emits the id of the media query that has changed. */ this.queryChangedSubject = new ReplaySubject(); } /** * Register a list of media queries that need to be tracked by the service. * * @param queries - A list of media queries that should be registered with the service. */ registerMediaQueries(...queries) { this.windowService.runInBrowser(({ browserWindow }) => { for (const [id, query] of queries) { // Wouter: Warn if the id has already been registered. if (this.queryIdMap.get(id)) { return console.warn(`NgxMediaQueryService: Media query with id '${id}' already exists and is defined by '${this.queryIdMap.get(id)}'`); } // Wouter: If the query has already been registered, throw an error to prevent duplicate subscriptions if ([...this.queryIdMap].some(([_, value]) => value === query)) { const duplicateQuery = [...this.queryIdMap].find(([_, value]) => value === query); throw new Error(`NgxMediaQueryService: Query of ['${id}', ${query}] already exists and is defined by ['${duplicateQuery[0]}', ${duplicateQuery[1]}]`); } // Wouter: save the id and query this.queryIdMap.set(id, query); // Wouter: For each query, create a MediaQueryList object const matchedQuery = browserWindow.matchMedia(query); // Wouter: Save the query this.queryListMap.set(id, matchedQuery); // Wouter: Emit the id of the query that has changed this.queryChangedSubject.next(id); // Wouter: Create a listener for the query. This is done separately to be // able to remove the listener when the service is destroyed const listener = (queryChangedEvent) => { this.queryListMap.set(id, queryChangedEvent.currentTarget); // Wouter: Emit the id of the query that has changed this.queryChangedSubject.next(id); }; // Wouter: Register the listener to the query matchedQuery.addEventListener('change', listener); // Wouter: Save the listener this.mediaQueryListenerMap.set(id, listener); } }); } /** * Pass the id of the query whose changes need to be listened to. * * @param id - The id of the media query that should be checked. * @returns An observable that emits a boolean value whenever the requested media query changes. */ getMatchingQuery$(id) { // Wouter: Throw an error if the query has not been registered if (!this.queryIdMap.has(id)) { throw new Error(`NgxMediaQueryService: No media query with id '${id}' has been registered. Please register the media query first using the 'registerMediaQueries' method.`); } return this.queryChangedSubject.asObservable().pipe( // Wouter: Filter the query that has changed. // This will make sure only the [id] streams are triggered. filter((queryId) => queryId === id), map(() => this.queryListMap.get(id).matches)); } /** * Unregister all media query subscriptions from the service. */ ngOnDestroy() { this.windowService.runInBrowser(() => { // Wouter: Remove all eventListeners for (const [id, query] of this.queryListMap) { query.removeEventListener('change', this.mediaQueryListenerMap.get(id)); } // Wouter: Complete subscriptions this.queryChangedSubject.next(null); this.queryChangedSubject.complete(); // Wouter: Clear maps this.queryListMap.clear(); this.mediaQueryListenerMap.clear(); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxMediaQueryService, deps: [{ token: i1.NgxWindowService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxMediaQueryService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxMediaQueryService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.NgxWindowService }] }); /** * A pipe that will search an array to see if on of its objects contains specific values on provided keys. * * Usage: * value | arrayContainsOne : ['prop1', 'prop2', ...] * * Examples: * {{ * [ * { title: 'This is the title', description: 'This is the description' }, * { title: 'This is the title' } * ] | arrayContainsOne: ['description'] * }} * Output: true */ class ArrayContainsOnePipe { transform(values, checkProps = []) { if (!Array.isArray(values) || values.length === 0 || !Array.isArray(checkProps) || checkProps.length === 0) { return false; } return values.some((item) => checkProps.some((key) => item[key] !== null && item[key] !== undefined)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: ArrayContainsOnePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: ArrayContainsOnePipe, isStandalone: true, name: "arrayContainsOne" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: ArrayContainsOnePipe, decorators: [{ type: Pipe, args: [{ name: 'arrayContainsOne', standalone: true, }] }] }); class BtwPipe { /** * Converts a BTW number to the correct format * * @param value - The value we wish to convert */ transform(value) { if (!value) { // Denis: if the value is falsy, return it without transform. return value; } const addCharAtIndex = (original, char, index) => { return original.slice(0, index) + char + original.slice(index); }; // Iben: Convert to string if it's a number value = value.toString(); if (value.replace(/\./g, '').length === 9) { value = '0' + value; } // Iben: Format: xxxx.xxx.xxx if (value.charAt(4) !== '.') { value = addCharAtIndex(value, '.', 4); } if (value.charAt(8) !== '.') { value = addCharAtIndex(value, '.', 8); } return value; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: BtwPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: BtwPipe, isStandalone: true, name: "btw" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: BtwPipe, decorators: [{ type: Pipe, args: [{ name: 'btw', standalone: true, }] }] }); class CleanArrayPipe { /** * Removes all falsy values from the provided array. * * @param value The values that need to be stripped of falsy values. * @param exceptions The falsy values that may be included in the filtered array. * @returns The filtered array. */ transform(value, exceptions = []) { if (!exceptions.length) { return value.filter(Boolean); } return value.filter((entry) => Boolean(entry) || exceptions.includes(entry)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: CleanArrayPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: CleanArrayPipe, isStandalone: true, name: "cleanArray" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: CleanArrayPipe, decorators: [{ type: Pipe, args: [{ name: 'cleanArray', standalone: true, }] }] }); class EntriesPipe { /** * Transforms a record into a [key, value] array * * @param value - The provided record */ transform(value) { // Iben: If there's no value or the value is not an object, we return an empty array to prevent frontend breaking if (!value || !isObject(value) || Array.isArray(value)) { return []; } // Iben: Transform the record to a [key,value] array return Array.from(Object.entries(value)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: EntriesPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: EntriesPipe, isStandalone: true, name: "entries" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: EntriesPipe, decorators: [{ type: Pipe, args: [{ name: 'entries', standalone: true, }] }] }); class HasObserversPipe { transform(output) { return output && output.observers.length > 0; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: HasObserversPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: HasObserversPipe, isStandalone: true, name: "hasObservers" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: HasObserversPipe, decorators: [{ type: Pipe, args: [{ name: 'hasObservers', standalone: true, }] }] }); class HasOwnProperty { /** * Checks whether the specified property exists within the given object. * * @param {unknown} object - The object to check for the presence of the property. * @param {string} prop - The property name to check for within the object. * @return {boolean} - Returns `true` if the property exists in the object, `false` otherwise. */ transform(object, prop) { // Use Object.prototype.hasOwnProperty to check if the property exists in the object const hasOwnProperty = Object.prototype.hasOwnProperty; // Check if the object is not null or undefined, and if it has the specified property return object != null && hasOwnProperty.call(object, prop); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: HasOwnProperty, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: HasOwnProperty, isStandalone: true, name: "hasOwnProperty" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: HasOwnProperty, decorators: [{ type: Pipe, args: [{ name: 'hasOwnProperty', standalone: true, }] }] }); class HasValuesPipe { /** * Checks whether an object has values * * @param value - The provided value */ transform(value) { // Iben: If the value is not an object, return valse if (!value || !isObject(value) || Array.isArray(value)) { return false; } // Iben: Return when the object has values return Object.keys(clean(value)).length > 0; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: HasValuesPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: HasValuesPipe, isStandalone: true, name: "hasValues" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: HasValuesPipe, decorators: [{ type: Pipe, args: [{ name: 'hasValues', standalone: true, }] }] }); class NgxHighlightPipe { /** * Highlights the provided substring of a text with a chosen dom element * * @param value - The full text with the part we wish to highlight * @param highlight - The part of the text we wish to highlight * @param config - The configuration to determine if we want to normalize the values, to be case-sensitive, which tag and/or class to use for the highlight * @param config.normalized - Default = true * @param config.caseInsensitive - Default = true * @param config.splitTextToHighlight - Default = false * @param config.someOrEveryMatch - Default = 'every' * @param config.tag - Default = 'mark' * @param config.highlightClass - Default = 'ngx-mark-highlight' */ transform(value, highlight, config) { // Femke: Setup configuration or defaults const normalized = config?.normalized ?? true; const caseInsensitive = config?.caseInsensitive ?? true; const splitTextToHighlight = config?.splitTextToHighlight ?? false; const someOrEveryMatch = config?.someOrEveryMatch || 'every'; const tag = config?.tag || 'mark'; const highlightClass = config?.highlightClass ?? 'ngx-mark-highlight'; // Femke: Early exit if there's no value/highlight or the value/highlight is not a string if (!value || !highlight || typeof value !== 'string' || typeof highlight !== 'string') { return value; } // Femke: determine which value to use (normalized or not => é â ö etc will be replaced with e a o) const usableValue = normalized ? normalizeString(value) : value; let usableHighlight = (normalized ? normalizeString(highlight) : highlight) // Femke: escape all regex characters .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if (splitTextToHighlight) { usableHighlight = usableHighlight // Femke: replace all multiple spaces next to each other to a single space .replace(/ +/gi, ' ') // Femke: replace space with a pipe to have an OR query instead of searching for the entire string eg search for 'some' OR 'thing' OR 'else' instead of 'some thing else' .replace(/ /gi, '|'); } // Femke: remove all multiple pipes at the start usableHighlight = usableHighlight.replace(/^\|+/gi, ''); // Femke: remember on what indices we updated in our search text let changedIndices = []; usableValue.split('').forEach((char, index) => { if (value[index] !== char) { changedIndices.push({ original: index, updated: index }); } }); let regexFlags = ''; // Femke: Every match should be highlighted, so the regex should apply global if (someOrEveryMatch === 'every') { regexFlags = 'g'; } // Femke: Regex should be case-insensitive if (caseInsensitive) { regexFlags += 'i'; } const regEx = new RegExp(usableHighlight, regexFlags); const usableClass = highlightClass ? ` class="${highlightClass}"` : ''; // Femke: Use a custom replacer so we can update our marked indices in case we found a match with our regex let replacedResult = usableValue.replace(regEx, (match, index) => { if (!match) { return ''; } const endIndex = index + match.length; // Femke: we are using the original text here to make sure we have the original accent characters within the highlight const result = `<${tag}${usableClass}>${value.substring(index, index + match.length)}</${tag}>`; // Femke: filter out all changed indices that lay withing the range of our current match changedIndices = changedIndices.filter((item) => !(index <= item.original && item.original < endIndex)); // Femke: update all found indices if they lay after the current replacement string // Femke: we are however keeping the original position to know with what we need to replace the character later on changedIndices = changedIndices.map((changedIndex) => { if (index >= changedIndex.original) { return changedIndex; } else { // Femke: take the current updated position (in case of multiple hits before this index // Femke: add the length of the replacement string but subtract the length of the match since that length was there already return { ...changedIndex, updated: changedIndex.updated + result.length - match.length, }; } }); return result; }); // Femke: Update the character on each index that was replaced when normalizing the string but was not part of any match of the search query changedIndices.forEach((item) => { replacedResult = replacedResult.substring(0, item.updated) + value.charAt(item.original) + replacedResult.substring(item.updated + 1); }); return replacedResult; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxHighlightPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: NgxHighlightPipe, isStandalone: true, name: "highlight" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxHighlightPipe, decorators: [{ type: Pipe, args: [{ name: 'highlight', }] }] }); class IbanPipe { transform(value = '') { value = value.replace(/\s/g, ''); // replace all spaces let reformat = value.replace(/(.{4})/g, function (match) { return match + ' '; // reformat into groups of 4 succeeded with a space }); reformat = reformat.trim(); // remove trailing space return reformat; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: IbanPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: IbanPipe, isStandalone: true, name: "IBAN" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: IbanPipe, decorators: [{ type: Pipe, args: [{ name: 'IBAN', standalone: true, }] }] }); class IsNotEmptyPipe { /** * Checks if a given argument is an object or array and if it is empty. * * @param value - can be any value. * @param checkProps - which props to check. */ transform(value, checkProps = []) { // Denis: check for array first, because "typeof Array" will return "object" if (Array.isArray(value) && value.length > 0) { return true; } // Denis: check for null values second, because "typeof null" will return "object" if (value === null) { return false; } // Denis: if the object is not empty, and checkProps is provided, check those props. if (typeof value === 'object' && checkProps.length > 0) { const propsWithValues = checkProps.filter((key) => typeof value[key] !== 'undefined' && value[key] !== null); return propsWithValues.length === checkProps.length; } // Denis: check for empty objects. if (typeof value === 'object' && Object.keys(value).length > 0) { return true; } return false; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: IsNotEmptyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: IsNotEmptyPipe, isStandalone: true, name: "isNotEmpty" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: IsNotEmptyPipe, decorators: [{ type: Pipe, args: [{ name: 'isNotEmpty', standalone: true, }] }] }); // Iben: This implementation is from https://github.com/nglrx/pipes/blob/master/packages/pipes/src/lib/array/join/join.pipe.ts // The package is no longer supported and is not compatible with more recent versions of Angular class JoinPipe { /** * Transforms an array to a joined string * * @param values - An array of values * @param separator - A separator we wish to use */ transform(values, separator) { return values && values.join(separator); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: JoinPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: JoinPipe, isStandalone: true, name: "join" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: JoinPipe, decorators: [{ type: Pipe, args: [{ name: 'join', standalone: true, }] }] }); class LimitToPipe { /** * Limits an array to a specific value * * @param value - The provided array * @param limitedTo - The number to which we want to limit the array */ transform(value, limitedTo) { // Iben: If no value is provided or the value is not an array, we early exit if (!value || !Array.isArray(value)) { return []; } // Iben: Slice the array based on the provided limit return [...value].slice(0, limitedTo); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: LimitToPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: LimitToPipe, isStandalone: true, name: "limitTo" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: LimitToPipe, decorators: [{ type: Pipe, args: [{ name: 'limitTo', standalone: true, }] }] }); class LogPipe { /** * Logs the provided value to the console. * * @param value The value to log to the console. * @param text An optional textual value to print before the piped value. */ transform(value, text) { text ? console.log(text, value) : console.log(value); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: LogPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: LogPipe, isStandalone: true, name: "log" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: LogPipe, decorators: [{ type: Pipe, args: [{ name: 'log', standalone: true, }] }] }); class MergeArraysPipe { /** * The mergeArrays pipe will take a source array and concat it with all provided additional arrays. * Undefined or null values are filtered from the flattened result. * * @param source{any[]} * @param arrays{any[]} */ transform(source = [], ...arrays) { // Denis: If the source is not a valid array, fallback to an empty array. if (!Array.isArray(source)) { return []; } return (source // Denis: Concat the source with the provided arguments, filter out the non-array values from the arguments. .concat(...arrays.filter((array) => Array.isArray(array))) // Denis: Filter undefined or null values from the flattened result. .filter((value) => typeof value !== 'undefined' && value !== null)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: MergeArraysPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.3", ngImport: i0, type: MergeArraysPipe, isStandalone: true, name: "mergeArrays" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: MergeArraysPipe, decorators: [{ type: Pipe, args: [{ name: 'mergeArrays', standalone: true, }] }] }); /** The configuration token for the NgxReplaceElementsPipe */ const NgxReplaceElementsConfigurationToken = new InjectionToken('NgxReplaceElementsConfigurationToken'); /** * A pipe that allows to replace text elements with a WebComponent */ class NgxReplaceElementsPipe { constructor(configuration, sanitizer) { this.configuration = configuration; this.sanitizer = sanitizer; } /** * Replaces all matches of a specific selector with provided WebComponents * * @param value - The original string value * @param items - The items we wish to replace */ transform(value, items) { // Iben: If the value isn't a string we early exit and warn the user if (typeof value !== 'string') { console.warn('NgxReplaceElements: A non string-value was provided to the NgxReplaceElementsPipe'); return ''; } // Iben: If no items were provided to replace, we just return the value if (!items || items.length === 0) { return value; } // Iben: set up a new instance of the DOMParser and parse the value as text/html. // This will return a Document which we can work with to find/replace elements. const parser = new DOMParser(); const body = parser.parseFromString(value, 'text/html'); // Iben: Loop over all items we wish to replace items.forEach((item) => { // Iben: Get the selector and the element we want to replace the target with const { selector, element, includeInnerText } = this.configuration[item.elementId]; // Iben: Select the target const target = body.querySelector(selector.replace('{{id}}', item.id)); // Iben: If no target was found, early exit if (!target) { return; } // Iben: Create a new element within the Do