diff --git a/CHANGELOG.md b/CHANGELOG.md index eedf1b79c..6721e3022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for a copy-to-clipboard functionality in the value component + ### Changed - Improved the language localization for Spanish (`es`) diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index 142d2ea53..f5a4e9c80 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -546,6 +546,10 @@ ngx-skeleton-loader { padding: 9px 24px !important; } +.no-height { + height: unset !important; +} + .no-min-width { min-width: unset !important; } diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index 14080c16d..c4d6532a7 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -4,7 +4,26 @@ }
- + + + + + + @if (enableCopyToClipboardButton) { + + } + + @if (value || value === 0 || value === null) {
} + @if (!hasLabel) { + + }
} @@ -88,6 +110,9 @@ @if (size === 'large') {
+ @if (hasLabel) { + + } @if (subLabel) { {{ subLabel }} } @@ -95,6 +120,9 @@ } @else { + @if (hasLabel) { + + } }
diff --git a/libs/ui/src/lib/value/value.component.stories.ts b/libs/ui/src/lib/value/value.component.stories.ts index eeebe9399..214331efc 100644 --- a/libs/ui/src/lib/value/value.component.stories.ts +++ b/libs/ui/src/lib/value/value.component.stories.ts @@ -1,3 +1,4 @@ +import '@angular/localize/init'; import { moduleMetadata } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -17,6 +18,9 @@ export default { control: 'select', options: ['desktop', 'mobile'] }, + enableCopyToClipboardButton: { + control: 'boolean' + }, size: { control: 'select', options: ['small', 'medium', 'large'] @@ -58,7 +62,7 @@ export const Label: Story = { }, render: (args) => ({ props: args, - template: `Label` + template: `Label` }) }; diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 795e16491..9c3330466 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -1,30 +1,41 @@ import { getLocale } from '@ghostfolio/common/helper'; +import { Clipboard } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; import { - CUSTOM_ELEMENTS_SCHEMA, + AfterViewInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, + computed, + CUSTOM_ELEMENTS_SCHEMA, + ElementRef, + input, Input, OnChanges, - computed, - input + 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 { copyOutline } from 'ionicons/icons'; import { isNumber } from 'lodash'; +import ms from 'ms'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, IonIcon, NgxSkeletonLoaderModule], + imports: [CommonModule, IonIcon, MatButtonModule, NgxSkeletonLoaderModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-value', 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 +48,26 @@ 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 changeDetectorRef: ChangeDetectorRef, + private clipboard: Clipboard, + private snackBar: MatSnackBar + ) { + addIcons({ + copyOutline + }); + } + public readonly precision = input(); private readonly formatOptions = computed(() => { @@ -59,6 +84,17 @@ export class GfValueComponent implements OnChanges { 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(); @@ -137,6 +173,18 @@ export class GfValueComponent implements OnChanges { } } + public onCopyValueToClipboard() { + this.clipboard.copy(String(this.value)); + + this.snackBar.open( + '✅ ' + $localize`${this.value} has been copied to the clipboard`, + undefined, + { + duration: ms('3 seconds') + } + ); + } + private initializeVariables() { this.absoluteValue = 0; this.formattedValue = '';