From 001bce86059150b7b5883ec23bd44b89c584396d Mon Sep 17 00:00:00 2001 From: Erwin-N Date: Sun, 15 Mar 2026 03:21:15 +0100 Subject: [PATCH] Add copy-to-clipboard functionality to value component --- libs/ui/src/lib/value/value.component.html | 27 +++++++- libs/ui/src/lib/value/value.component.scss | 4 ++ .../src/lib/value/value.component.stories.ts | 26 ++++++++ libs/ui/src/lib/value/value.component.ts | 65 +++++++++++++++---- 4 files changed, 110 insertions(+), 12 deletions(-) diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index 14080c16d..58211ac01 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -4,7 +4,23 @@ }
- + + + + + + @if (enableCopyToClipboardButton) { + + + + } + + @if (value || value === 0 || value === null) {
} + @if (!hasLabel) { + + }
} @@ -88,6 +107,9 @@ @if (size === 'large') {
+ @if (hasLabel) { + + } @if (subLabel) { {{ subLabel }} } @@ -95,6 +117,9 @@ } @else { + @if (hasLabel) { + + } }
diff --git a/libs/ui/src/lib/value/value.component.scss b/libs/ui/src/lib/value/value.component.scss index 087bf31e1..7df2d17c4 100644 --- a/libs/ui/src/lib/value/value.component.scss +++ b/libs/ui/src/lib/value/value.component.scss @@ -7,4 +7,8 @@ font-variant-numeric: initial; line-height: 1; } + .mdc-copy { + display: inline-flex; + vertical-align: middle; + } } diff --git a/libs/ui/src/lib/value/value.component.stories.ts b/libs/ui/src/lib/value/value.component.stories.ts index eeebe9399..aea666644 100644 --- a/libs/ui/src/lib/value/value.component.stories.ts +++ b/libs/ui/src/lib/value/value.component.stories.ts @@ -32,6 +32,32 @@ export const Loading: Story = { } }; +export const IsinNoLabel: Story = { + args: { + isCurrency: false, + locale: 'en-US', + unit: 'USD', + value: 'US5949181045', + enableCopyToClipboardButton: true + }, + name: 'Without Label with Copy' +}; + +export const IsinWithLabel: Story = { + args: { + isCurrency: false, + locale: 'en-US', + unit: 'USD', + value: 'US5949181045', + enableCopyToClipboardButton: true + }, + render: (args) => ({ + props: args, + template: `ISIN` + }), + name: 'With Label and Copy' +}; + export const Currency: Story = { args: { isCurrency: true, diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 795e16491..39565fd9e 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -1,17 +1,25 @@ import { getLocale } from '@ghostfolio/common/helper'; +import { Clipboard } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; import { - CUSTOM_ELEMENTS_SCHEMA, + AfterViewInit, ChangeDetectionStrategy, Component, + computed, + CUSTOM_ELEMENTS_SCHEMA, + ElementRef, + input, Input, OnChanges, - computed, - input + ViewChild } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { IonIcon } from '@ionic/angular/standalone'; +import { addIcons } from 'ionicons'; +import { copyOutline } from 'ionicons/icons'; import { isNumber } from 'lodash'; +import ms from 'ms'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ @@ -22,9 +30,10 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; styleUrls: ['./value.component.scss'], templateUrl: './value.component.html' }) -export class GfValueComponent implements OnChanges { +export class GfValueComponent implements AfterViewInit, OnChanges { @Input() colorizeSign = false; @Input() deviceType: string; + @Input() enableCopyToClipboardButton = false; @Input() icon = ''; @Input() isAbsolute = false; @Input() isCurrency = false; @@ -37,12 +46,24 @@ export class GfValueComponent implements OnChanges { @Input() unit = ''; @Input() value: number | string = ''; + @ViewChild('labelContent', { static: false }) labelContent!: ElementRef; + public absoluteValue = 0; public formattedValue = ''; + public hasLabel = false; public isNumber = false; public isString = false; public useAbsoluteValue = false; + public constructor( + private clipboard: Clipboard, + private snackBar: MatSnackBar + ) { + addIcons({ + copyOutline + }); + } + public readonly precision = input(); private readonly formatOptions = computed(() => { @@ -59,6 +80,20 @@ export class GfValueComponent implements OnChanges { return precision !== undefined && precision >= 0; } + private initializeVariables() { + this.absoluteValue = 0; + this.formattedValue = ''; + this.isNumber = false; + this.isString = false; + this.locale = this.locale || getLocale(); + this.useAbsoluteValue = false; + } + + public ngAfterViewInit() { + const el = this.labelContent.nativeElement; + this.hasLabel = el.textContent.trim().length > 0 || el.children.length > 0; + } + public ngOnChanges() { this.initializeVariables(); @@ -137,12 +172,20 @@ export class GfValueComponent implements OnChanges { } } - private initializeVariables() { - this.absoluteValue = 0; - this.formattedValue = ''; - this.isNumber = false; - this.isString = false; - this.locale = this.locale || getLocale(); - this.useAbsoluteValue = false; + public onCopyValueToClipboard() { + if (this.value || this.value === 0) { + this.clipboard.copy(this.formattedValue); + this.snackBar.open( + '✅ ' + $localize`Value has been copied to the clipboard`, + undefined, + { + duration: ms('3 seconds') + } + ); + } else { + this.snackBar.open($localize`Value is empty`, undefined, { + duration: ms('3 seconds') + }); + } } }