Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"amplitude-js": "^8.21.9",
"angular-password-strength-meter": "npm:@eresearchqut/angular-password-strength-meter@^13.0.7",
"angulartics2": "^14.1.0",
"color-string": "^2.0.1",
"convert": "^5.12.0",
"date-fns": "^4.1.0",
"ipaddr.js": "^2.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,19 @@ export class DbTableWidgetsComponent implements OnInit {
"allow_negative": true
}
`,
Color: `// No settings required
// You can use this field to display colors in hex format, like #FF5733 or #333.`,
Color: `// Optional: Specify output format for color values
// Supported formats: "hex", "hex_hash" (default), "rgb", "hsl"
// Example configuration:

{
"format": "hex_hash" // Will display colors as "#FF5733"
}

// Format options:
// - "hex": Display as "FF5733" (no hash)
// - "hex_hash": Display as "#FF5733" (default)
// - "rgb": Display as "rgb(255, 87, 51)"
// - "hsl": Display as "hsl(9, 100%, 60%)"`,
}

constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
[required]="required" [disabled]="disabled" [readonly]="readonly"
attr.data-testid="record-{{label}}-color"
[(ngModel)]="value" (ngModelChange)="onTextInputChange()"
placeholder="#000000"
pattern="^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$">
placeholder="e.g. #000000, rgb(0,0,0), hsl(0,0%,0%)">
<div matSuffix class="color-picker-container">
<input type="color"
class="color-picker-input"
[value]="isValidColor ? value : '#000000'"
[value]="normalizedColorForPicker"
[disabled]="disabled || readonly"
(change)="onColorPickerChange($event)"
title="Choose color">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import colorString from 'color-string';

@Injectable()

Expand All @@ -20,13 +21,105 @@ export class ColorEditComponent extends BaseEditFieldComponent {

get isValidColor(): boolean {
if (!this.value) return false;
const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
return colorRegex.test(this.value);
return this.parseColor(this.value) !== null;
}

get normalizedColorForPicker(): string {
const parsed = this.parseColor(this.value);
if (parsed) {
const [r, g, b] = parsed.value;
return `#${this.toHex(r)}${this.toHex(g)}${this.toHex(b)}`;
}
return '#000000';
}

get formattedColorValue(): string {
const parsed = this.parseColor(this.value);
if (!parsed) return this.value;

const format = this.widgetStructure?.widget_params?.format || 'hex_hash';
const [r, g, b, a] = parsed.value;

switch (format) {
case 'hex':
return colorString.to.hex(r, g, b, a).slice(1); // Remove # prefix
case 'hex_hash':
return colorString.to.hex(r, g, b, a);
case 'rgb':
return colorString.to.rgb(r, g, b, a);
case 'hsl':
// Convert RGB to HSL using built-in conversion
const hex = colorString.to.hex(r, g, b, a);
const hslParsed = colorString.get.hsl(hex);
if (hslParsed) {
const [h, s, l, alpha] = hslParsed;
return colorString.to.hsl(h, s, l, alpha);
}
return hex;
default:
return colorString.to.hex(r, g, b, a);
}
}

private parseColor(color: string): any {
if (!color) return null;

// Try parsing with color-string
const parsed = colorString.get(color);
if (parsed) return parsed;

// Try hex without hash
if (/^[A-Fa-f0-9]{6}$|^[A-Fa-f0-9]{3}$/.test(color)) {
return colorString.get('#' + color);
}

return null;
}

private toHex(n: number): string {
const hex = n.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}

onColorPickerChange(event: Event) {
const target = event.target as HTMLInputElement;
this.value = target.value;
const pickerValue = target.value;

// Convert picker value to desired format
const parsed = this.parseColor(pickerValue);
if (parsed) {
const format = this.widgetStructure?.widget_params?.format || 'hex_hash';

const [r, g, b, a] = parsed.value;

switch (format) {
case 'hex':
this.value = colorString.to.hex(r, g, b, a).slice(1);
break;
case 'hex_hash':
this.value = colorString.to.hex(r, g, b, a);
break;
case 'rgb':
this.value = colorString.to.rgb(r, g, b, a);
break;
case 'hsl':
// Convert RGB to HSL using built-in conversion
const hex = colorString.to.hex(r, g, b, a);
const hslParsed = colorString.get.hsl(hex);
if (hslParsed) {
const [h, s, l, alpha] = hslParsed;
this.value = colorString.to.hsl(h, s, l, alpha);
} else {
this.value = hex;
}
break;
default:
this.value = colorString.to.hex(r, g, b, a);
}
} else {
this.value = pickerValue;
}

this.onFieldChange.emit(this.value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="color-display">
<div *ngIf="isValidColor"
class="color-swatch"
[style.background-color]="value"
[style.background-color]="normalizedColorForDisplay"
[title]="value || 'No color'"
></div>
<span class="field-value">{{value || '—'}}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { NgIf } from '@angular/common';
import colorString from 'color-string';

@Injectable()
@Component({
Expand All @@ -17,7 +18,36 @@ import { NgIf } from '@angular/common';
export class ColorDisplayComponent extends BaseTableDisplayFieldComponent {
get isValidColor(): boolean {
if (!this.value) return false;
const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
return colorRegex.test(this.value);
return this.parseColor(this.value) !== null;
}

get normalizedColorForDisplay(): string {
const parsed = this.parseColor(this.value);
if (parsed) {
const [r, g, b] = parsed.value;
return `#${this.toHex(r)}${this.toHex(g)}${this.toHex(b)}`;
}
return '#000000';
}


private parseColor(color: string): any {
if (!color) return null;

// Try parsing with color-string
const parsed = colorString.get(color);
if (parsed) return parsed;

// Try hex without hash
if (/^[A-Fa-f0-9]{6}$|^[A-Fa-f0-9]{3}$/.test(color)) {
return colorString.get('#' + color);
}

return null;
}

private toHex(n: number): string {
const hex = n.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
}
17 changes: 17 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5691,13 +5691,29 @@ __metadata:
languageName: node
linkType: hard

"color-name@npm:^2.0.0":
version: 2.0.0
resolution: "color-name@npm:2.0.0"
checksum: 10a1addae41de2987d6b90dbd3cfade266c2e6f680ce21749911df4493b4fae07654862c6b5358bdd13e155461acb4eedaa5e0ba172bf13542cdcca10866cf2b
languageName: node
linkType: hard

"color-name@npm:~1.1.4":
version: 1.1.4
resolution: "color-name@npm:1.1.4"
checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610
languageName: node
linkType: hard

"color-string@npm:^2.0.1":
version: 2.0.1
resolution: "color-string@npm:2.0.1"
dependencies:
color-name: ^2.0.0
checksum: a5ab024f78f67c0d5c1c995943ff95dce193beaa981492f6a36c05a9939a9db519e2d821d91df15677a47107fe90e4b12fd345729d755c65b543924db05c3a3f
languageName: node
linkType: hard

"colorette@npm:^2.0.10, colorette@npm:^2.0.20":
version: 2.0.20
resolution: "colorette@npm:2.0.20"
Expand Down Expand Up @@ -6655,6 +6671,7 @@ __metadata:
amplitude-js: ^8.21.9
angular-password-strength-meter: "npm:@eresearchqut/angular-password-strength-meter@^13.0.7"
angulartics2: ^14.1.0
color-string: ^2.0.1
convert: ^5.12.0
date-fns: ^4.1.0
ipaddr.js: ^2.2.0
Expand Down