mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-25603] Use angular template for avatar (#15978)
We should be able to simplify the angular component by using svg element directly.
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
[color]="currentAccount.avatarColor"
|
||||
size="small"
|
||||
aria-hidden="true"
|
||||
class="[&>img]:tw-block"
|
||||
></bit-avatar>
|
||||
</button>
|
||||
<ng-template #defaultButton>
|
||||
|
||||
@@ -137,3 +137,8 @@ body.body-full {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Temporary fix for avatar, will not be required once we migrate to tailwind preflight **/
|
||||
bit-avatar svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -453,7 +453,7 @@ export class Utils {
|
||||
};
|
||||
}
|
||||
|
||||
static isNullOrWhitespace(str: string): boolean {
|
||||
static isNullOrWhitespace(str: string | null | undefined): boolean {
|
||||
return str == null || typeof str !== "string" || str.trim() === "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, OnChanges, input } from "@angular/core";
|
||||
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
|
||||
import { Component, computed, input } from "@angular/core";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -22,12 +21,34 @@ const SizeClasses: Record<SizeTypes, string[]> = {
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-avatar",
|
||||
template: `@if (src) {
|
||||
<img [src]="src" title="{{ title() || text() }}" [ngClass]="classList" />
|
||||
}`,
|
||||
template: `
|
||||
<span [title]="title() || text()">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
pointer-events="none"
|
||||
[style.backgroundColor]="backgroundColor()"
|
||||
[ngClass]="classList()"
|
||||
attr.viewBox="0 0 {{ svgSize }} {{ svgSize }}"
|
||||
>
|
||||
<text
|
||||
text-anchor="middle"
|
||||
y="50%"
|
||||
x="50%"
|
||||
dy="0.35em"
|
||||
pointer-events="auto"
|
||||
[attr.fill]="textColor()"
|
||||
[style.fontWeight]="svgFontWeight"
|
||||
[style.fontSize.px]="svgFontSize"
|
||||
font-family='Roboto,"Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||
>
|
||||
{{ displayChars() }}
|
||||
</text>
|
||||
</svg>
|
||||
</span>
|
||||
`,
|
||||
imports: [NgClass],
|
||||
})
|
||||
export class AvatarComponent implements OnChanges {
|
||||
export class AvatarComponent {
|
||||
readonly border = input(false);
|
||||
readonly color = input<string>();
|
||||
readonly id = input<string>();
|
||||
@@ -35,36 +56,40 @@ export class AvatarComponent implements OnChanges {
|
||||
readonly title = input<string>();
|
||||
readonly size = input<SizeTypes>("default");
|
||||
|
||||
private svgCharCount = 2;
|
||||
private svgFontSize = 20;
|
||||
private svgFontWeight = 300;
|
||||
private svgSize = 48;
|
||||
src?: SafeResourceUrl;
|
||||
protected readonly svgCharCount = 2;
|
||||
protected readonly svgFontSize = 20;
|
||||
protected readonly svgFontWeight = 300;
|
||||
protected readonly svgSize = 48;
|
||||
|
||||
constructor(public sanitizer: DomSanitizer) {}
|
||||
|
||||
ngOnChanges() {
|
||||
this.generate();
|
||||
}
|
||||
|
||||
get classList() {
|
||||
return ["tw-rounded-full", "tw-inline"]
|
||||
protected readonly classList = computed(() => {
|
||||
return ["tw-rounded-full"]
|
||||
.concat(SizeClasses[this.size()] ?? [])
|
||||
.concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
|
||||
}
|
||||
});
|
||||
|
||||
private generate() {
|
||||
const color = this.color();
|
||||
const text = this.text();
|
||||
protected readonly backgroundColor = computed(() => {
|
||||
const id = this.id();
|
||||
if (!text && !color && !id) {
|
||||
throw new Error("Must supply `text`, `color`, or `id` input.");
|
||||
const upperCaseText = this.text()?.toUpperCase() ?? "";
|
||||
|
||||
if (!Utils.isNullOrWhitespace(this.color())) {
|
||||
return this.color()!;
|
||||
}
|
||||
let chars: string | null = null;
|
||||
const upperCaseText = text?.toUpperCase() ?? "";
|
||||
|
||||
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
|
||||
if (!Utils.isNullOrWhitespace(id)) {
|
||||
return Utils.stringToColor(id!.toString());
|
||||
}
|
||||
|
||||
return Utils.stringToColor(upperCaseText);
|
||||
});
|
||||
|
||||
protected readonly textColor = computed(() => {
|
||||
return Utils.pickTextColorBasedOnBgColor(this.backgroundColor(), 135, true);
|
||||
});
|
||||
|
||||
protected readonly displayChars = computed(() => {
|
||||
const upperCaseText = this.text()?.toUpperCase() ?? "";
|
||||
|
||||
let chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
|
||||
if (chars == null) {
|
||||
chars = this.unicodeSafeSubstring(upperCaseText, this.svgCharCount);
|
||||
}
|
||||
@@ -75,30 +100,10 @@ export class AvatarComponent implements OnChanges {
|
||||
chars = emojiMatch[0];
|
||||
}
|
||||
|
||||
let svg: HTMLElement;
|
||||
let hexColor = color ?? "";
|
||||
if (!Utils.isNullOrWhitespace(hexColor)) {
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
} else if (!Utils.isNullOrWhitespace(id ?? "")) {
|
||||
hexColor = Utils.stringToColor(id!.toString());
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
} else {
|
||||
hexColor = Utils.stringToColor(upperCaseText);
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
}
|
||||
return chars;
|
||||
});
|
||||
|
||||
const charObj = this.createTextElement(chars, hexColor);
|
||||
svg.appendChild(charObj);
|
||||
const html = window.document.createElement("div").appendChild(svg).outerHTML;
|
||||
const svgHtml = window.btoa(unescape(encodeURIComponent(html)));
|
||||
|
||||
// This is safe because the only user provided value, chars is set using `textContent`
|
||||
this.src = this.sanitizer.bypassSecurityTrustResourceUrl(
|
||||
"data:image/svg+xml;base64," + svgHtml,
|
||||
);
|
||||
}
|
||||
|
||||
private getFirstLetters(data: string, count: number): string | null {
|
||||
private getFirstLetters(data: string, count: number): string | undefined {
|
||||
const parts = data.split(" ");
|
||||
if (parts.length > 1) {
|
||||
let text = "";
|
||||
@@ -107,39 +112,7 @@ export class AvatarComponent implements OnChanges {
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private createSvgElement(size: number, color: string): HTMLElement {
|
||||
const svgTag = window.document.createElement("svg");
|
||||
svgTag.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
svgTag.setAttribute("pointer-events", "none");
|
||||
svgTag.setAttribute("width", size.toString());
|
||||
svgTag.setAttribute("height", size.toString());
|
||||
svgTag.style.backgroundColor = color;
|
||||
svgTag.style.width = size + "px";
|
||||
svgTag.style.height = size + "px";
|
||||
return svgTag;
|
||||
}
|
||||
|
||||
private createTextElement(character: string, color: string): HTMLElement {
|
||||
const textTag = window.document.createElement("text");
|
||||
textTag.setAttribute("text-anchor", "middle");
|
||||
textTag.setAttribute("y", "50%");
|
||||
textTag.setAttribute("x", "50%");
|
||||
textTag.setAttribute("dy", "0.35em");
|
||||
textTag.setAttribute("pointer-events", "auto");
|
||||
textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true));
|
||||
textTag.setAttribute(
|
||||
"font-family",
|
||||
'Roboto,"Helvetica Neue",Helvetica,Arial,' +
|
||||
'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
|
||||
);
|
||||
// Warning do not use innerHTML here, characters are user provided
|
||||
textTag.textContent = character;
|
||||
textTag.style.fontWeight = this.svgFontWeight.toString();
|
||||
textTag.style.fontSize = this.svgFontSize + "px";
|
||||
return textTag;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private unicodeSafeSubstring(str: string, count: number) {
|
||||
|
||||
Reference in New Issue
Block a user