import { getLocale } from '@ghostfolio/common/helper'; import { Clipboard } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, CUSTOM_ELEMENTS_SCHEMA, ElementRef, input, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatSnackBar } from '@angular/material/snack-bar'; import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { checkmarkOutline, copyOutline } from 'ionicons/icons'; import { isNumber } from 'lodash-es'; import ms from 'ms'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, IonIcon, MatButtonModule, NgxSkeletonLoaderModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-value', styleUrls: ['./value.component.scss'], templateUrl: './value.component.html' }) export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() colorizeSign = false; @Input() deviceType: string; @Input() enableCopyToClipboardButton = false; @Input() icon = ''; @Input() isAbsolute = false; @Input() isCurrency = false; @Input() isDate = false; @Input() isPercent = false; @Input() locale?: string; @Input() position = ''; @Input() size: 'large' | 'medium' | 'small' = 'small'; @Input() subLabel?: string = ''; @Input() unit?: string | null = ''; @Input() value?: Date | number | string | null = ''; @ViewChild('labelContent', { static: false }) labelContent!: ElementRef; public absoluteValue = 0; public formattedValue = ''; public hasLabel = false; public isCopied = false; public isNumber = false; public isString = false; public useAbsoluteValue = false; public readonly copiedTitle = $localize`The value has been copied to the clipboard`; public readonly copyToClipboardTitle = $localize`Copy to clipboard`; public readonly isLoading = input(false); public readonly precision = input(); public constructor( private changeDetectorRef: ChangeDetectorRef, private clipboard: Clipboard, private snackBar: MatSnackBar ) { addIcons({ checkmarkOutline, copyOutline }); } private copyToClipboardTimeout: ReturnType; private readonly formatOptions = computed(() => { const digits = this.hasPrecision ? this.precision() : 2; return { maximumFractionDigits: digits, minimumFractionDigits: digits }; }); private get hasPrecision() { const precision = this.precision(); return precision !== undefined && precision >= 0; } public ngAfterViewInit() { if (this.labelContent) { const element = this.labelContent.nativeElement; this.hasLabel = element.children.length > 0 || element.textContent.trim().length > 0; this.changeDetectorRef.markForCheck(); } } public ngOnChanges() { this.initializeVariables(); if (this.value || this.value === 0) { if (isNumber(this.value)) { this.isNumber = true; this.isString = false; this.absoluteValue = Math.abs(this.value); if (this.colorizeSign) { if (this.isCurrency) { try { this.formattedValue = this.absoluteValue.toLocaleString( this.locale, this.formatOptions() ); } catch {} } else if (this.isPercent) { try { this.formattedValue = (this.absoluteValue * 100).toLocaleString( this.locale, this.formatOptions() ); } catch {} } } else if (this.isCurrency) { try { this.formattedValue = this.value?.toLocaleString( this.locale, this.formatOptions() ); } catch {} } else if (this.isPercent) { try { this.formattedValue = (this.value * 100).toLocaleString( this.locale, this.formatOptions() ); } catch {} } else if (this.hasPrecision) { try { this.formattedValue = this.value?.toLocaleString( this.locale, this.formatOptions() ); } catch {} } else { this.formattedValue = this.value?.toLocaleString(this.locale); } if (this.isAbsolute) { // Remove algebraic sign this.formattedValue = this.formattedValue.replace(/^-/, ''); } } else { this.isNumber = false; this.isString = true; if (this.isDate) { this.formattedValue = new Date(this.value).toLocaleDateString( this.locale, { day: '2-digit', month: '2-digit', year: this.deviceType === 'mobile' ? '2-digit' : 'numeric' } ); } else { this.formattedValue = String(this.value); } } } if (/^-?0([.,]0*)?$/.test(this.formattedValue)) { // Remove algebraic sign of values rounding to zero this.formattedValue = this.formattedValue.replace(/^-/, ''); this.useAbsoluteValue = true; } } public onCopyValueToClipboard() { this.clipboard.copy(String(this.value)); this.isCopied = true; if (this.copyToClipboardTimeout) { clearTimeout(this.copyToClipboardTimeout); } this.copyToClipboardTimeout = setTimeout(() => { this.isCopied = false; this.changeDetectorRef.markForCheck(); }, ms('3 seconds')); this.snackBar.open( '✅ ' + $localize`${this.value} has been copied to the clipboard`, undefined, { duration: ms('3 seconds') } ); } public ngOnDestroy() { if (this.copyToClipboardTimeout) { clearTimeout(this.copyToClipboardTimeout); } } private initializeVariables() { this.absoluteValue = 0; this.formattedValue = ''; this.isCopied = false; this.isNumber = false; this.isString = false; this.useAbsoluteValue = false; if (this.copyToClipboardTimeout) { clearTimeout(this.copyToClipboardTimeout); } if (this.locale === undefined || this.locale === '') { this.locale = getLocale(); } } }