UNPKG

ngx-avatar

Version:

A universal avatar component for Angular applications that fetches / generates avatar based on the information you have about the user.

737 lines (714 loc) 21.9 kB
import { Injectable, InjectionToken, Optional, Inject, EventEmitter, Component, Input, Output, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Md5 } from 'ts-md5'; import { HttpClient } from '@angular/common/http'; import { takeWhile, map } from 'rxjs/operators'; /** * Contract of all async sources. * Every async source must implement the processResponse method that extracts the avatar url from the data */ class AsyncSource { constructor(sourceId) { this.sourceId = sourceId; } } var AvatarSource; (function (AvatarSource) { AvatarSource["FACEBOOK"] = "facebook"; AvatarSource["GOOGLE"] = "google"; AvatarSource["TWITTER"] = "twitter"; AvatarSource["INSTAGRAM"] = "instagram"; AvatarSource["VKONTAKTE"] = "vkontakte"; AvatarSource["SKYPE"] = "skype"; AvatarSource["GRAVATAR"] = "gravatar"; AvatarSource["GITHUB"] = "github"; AvatarSource["CUSTOM"] = "custom"; AvatarSource["INITIALS"] = "initials"; AvatarSource["VALUE"] = "value"; })(AvatarSource || (AvatarSource = {})); /** * Facebook source implementation. * Fetch avatar source based on facebook identifier * and image size */ class Facebook { constructor(sourceId) { this.sourceId = sourceId; this.sourceType = AvatarSource.FACEBOOK; } getAvatar(size) { return ('https://graph.facebook.com/' + `${this.sourceId}/picture?width=${size}&height=${size}`); } } /** * Twitter source implementation. * Fetch avatar source based on google identifier * and image size */ class Twitter { constructor(sourceId) { this.sourceId = sourceId; this.sourceType = AvatarSource.TWITTER; } getAvatar(size) { const twitterImgSize = this.getImageSize(size); return `https://twitter.com/${this.sourceId}/profile_image?size=${twitterImgSize}`; } getImageSize(size) { if (size <= 24) { return 'mini'; } if (size <= 48) { return 'normal'; } if (size <= 73) { return 'bigger'; } return 'original'; } } /** * Google source implementation. * Fetch avatar source based on google identifier * and image size */ class Google extends AsyncSource { constructor(sourceId) { super(sourceId); this.sourceType = AvatarSource.GOOGLE; } getAvatar() { return `https://picasaweb.google.com/data/entry/api/user/${this.sourceId}?alt=json`; } /** * Extract google avatar from json data */ processResponse(data, size) { const avatarSrc = data.entry.gphoto$thumbnail.$t; if (avatarSrc) { return avatarSrc.replace('s64', 's' + size); } return null; } } /** * Instagram source impelementation. * Fetch avatar source based on instagram identifier */ class Instagram extends AsyncSource { constructor(sourceId) { super(sourceId); this.sourceType = AvatarSource.INSTAGRAM; } getAvatar() { return `https://www.instagram.com/${this.sourceId}/?__a=1`; } /** * extract instagram avatar from json data */ processResponse(data, size) { return `${data.graphql.user.profile_pic_url_hd}&s=${size}`; } } /** * Custom source implementation. * return custom image as an avatar * */ class Custom { constructor(sourceId) { this.sourceId = sourceId; this.sourceType = AvatarSource.CUSTOM; } getAvatar() { return this.sourceId; } } /** * Initials source implementation. * return the initials of the given value */ class Initials { constructor(sourceId) { this.sourceId = sourceId; this.sourceType = AvatarSource.INITIALS; } getAvatar(size) { return this.getInitials(this.sourceId, size); } /** * Returns the initial letters of a name in a string. */ getInitials(name, size) { name = name.trim(); if (!name) { return ''; } const initials = name.split(' '); if (size && size < initials.length) { return this.constructInitials(initials.slice(0, size)); } else { return this.constructInitials(initials); } } /** * Iterates a person's name string to get the initials of each word in uppercase. */ constructInitials(elements) { if (!elements || !elements.length) { return ''; } return elements .filter(element => element && element.length > 0) .map(element => element[0].toUpperCase()) .join(''); } } function isRetina() { if (typeof window !== 'undefined' && window !== null) { if (window.devicePixelRatio > 1.25) { return true; } const mediaQuery = '(-webkit-min-device-pixel-ratio: 1.25), (min--moz-device-pixel-ratio: 1.25), (-o-min-device-pixel-ratio: 5/4), (min-resolution: 1.25dppx)'; if (window.matchMedia && window.matchMedia(mediaQuery).matches) { return true; } } return false; } /** * Gravatar source implementation. * Fetch avatar source based on gravatar email */ class Gravatar { constructor(value) { this.value = value; this.sourceType = AvatarSource.GRAVATAR; this.sourceId = value.match('^[a-f0-9]{32}$') ? value : Md5.hashStr(value).toString(); } getAvatar(size) { const avatarSize = isRetina() ? size * 2 : size; return `https://secure.gravatar.com/avatar/${this.sourceId}?s=${avatarSize}&d=404`; } } /** * Skype source implementation. * Fetch avatar source based on skype identifier */ class Skype { constructor(sourceId) { this.sourceId = sourceId; this.sourceType = AvatarSource.SKYPE; } getAvatar() { return `https://api.skype.com/users/${this.sourceId}/profile/avatar`; } } /** * Value source implementation. * return the value as avatar */ class Value { constructor(sourceId) { this.sourceId = sourceId; this.sourceType = AvatarSource.VALUE; } getAvatar() { return this.sourceId; } } /** * Vkontakte source implementation. * Fetch avatar source based on vkontakte identifier * and image size */ const apiVersion = 5.8; class Vkontakte extends AsyncSource { constructor(sourceId) { super(sourceId); this.sourceType = AvatarSource.VKONTAKTE; } getAvatar(size) { const imgSize = this.getImageSize(size); return `https://api.vk.com/method/users.get?user_id=${this.sourceId}&v=${apiVersion}&fields=${imgSize}`; } /** * extract vkontakte avatar from json data */ processResponse(data) { // avatar key property is the size used to generate avatar url // size property is always the last key in the response object const sizeProperty = Object.keys(data['response'][0]).pop(); if (!sizeProperty) { return null; } // return avatar src return data['response'][0][sizeProperty] || null; } /** * Returns image size related to vkontakte API */ getImageSize(size) { if (size <= 50) { return 'photo_50'; } if (size <= 100) { return 'photo_100'; } if (size <= 200) { return 'photo_200'; } return 'photo_max'; } } /** * GitHub source implementation. * Fetch avatar source based on github identifier */ class Github extends AsyncSource { constructor(sourceId) { super(sourceId); this.sourceType = AvatarSource.GITHUB; } getAvatar() { return `https://api.github.com/users/${this.sourceId}`; } /** * extract github avatar from json data */ processResponse(data, size) { if (size) { return `${data.avatar_url}&s=${size}`; } return data.avatar_url; } } /** * Factory class that implements factory method pattern. * Used to create Source implementation class based * on the source Type */ class SourceFactory { constructor() { this.sources = {}; this.sources[AvatarSource.FACEBOOK] = Facebook; this.sources[AvatarSource.TWITTER] = Twitter; this.sources[AvatarSource.GOOGLE] = Google; this.sources[AvatarSource.INSTAGRAM] = Instagram; this.sources[AvatarSource.SKYPE] = Skype; this.sources[AvatarSource.GRAVATAR] = Gravatar; this.sources[AvatarSource.CUSTOM] = Custom; this.sources[AvatarSource.INITIALS] = Initials; this.sources[AvatarSource.VALUE] = Value; this.sources[AvatarSource.VKONTAKTE] = Vkontakte; this.sources[AvatarSource.GITHUB] = Github; } newInstance(sourceType, sourceValue) { return new this.sources[sourceType](sourceValue); } } SourceFactory.decorators = [ { type: Injectable } ]; SourceFactory.ctorParameters = () => []; /** * Token used to inject the AvatarConfig object */ const AVATAR_CONFIG = new InjectionToken('avatar.config'); class AvatarConfigService { constructor(userConfig) { this.userConfig = userConfig; } getAvatarSources(defaultSources) { if (this.userConfig && this.userConfig.sourcePriorityOrder && this.userConfig.sourcePriorityOrder.length) { const uniqueSources = [...new Set(this.userConfig.sourcePriorityOrder)]; const validSources = uniqueSources.filter(source => defaultSources.includes(source)); return [ ...validSources, ...defaultSources.filter(source => !validSources.includes(source)) ]; } return defaultSources; } getAvatarColors(defaultColors) { return ((this.userConfig && this.userConfig.colors && this.userConfig.colors.length && this.userConfig.colors) || defaultColors); } } AvatarConfigService.decorators = [ { type: Injectable } ]; AvatarConfigService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [AVATAR_CONFIG,] }] } ]; /** * list of Supported avatar sources */ const defaultSources = [ AvatarSource.FACEBOOK, AvatarSource.GOOGLE, AvatarSource.TWITTER, AvatarSource.INSTAGRAM, AvatarSource.VKONTAKTE, AvatarSource.SKYPE, AvatarSource.GRAVATAR, AvatarSource.GITHUB, AvatarSource.CUSTOM, AvatarSource.INITIALS, AvatarSource.VALUE ]; /** * list of default colors */ const defaultColors = [ '#1abc9c', '#3498db', '#f1c40f', '#8e44ad', '#e74c3c', '#d35400', '#2c3e50', '#7f8c8d' ]; /** * Provides utilities methods related to Avatar component */ class AvatarService { constructor(http, avatarConfigService) { this.http = http; this.avatarConfigService = avatarConfigService; this.avatarSources = defaultSources; this.avatarColors = defaultColors; this.failedSources = new Map(); this.overrideAvatarSources(); this.overrideAvatarColors(); } fetchAvatar(avatarUrl) { return this.http.get(avatarUrl); } getRandomColor(avatarText) { if (!avatarText) { return 'transparent'; } const asciiCodeSum = this.calculateAsciiCode(avatarText); return this.avatarColors[asciiCodeSum % this.avatarColors.length]; } compareSources(sourceType1, sourceType2) { return (this.getSourcePriority(sourceType1) - this.getSourcePriority(sourceType2)); } isSource(source) { return this.avatarSources.includes(source); } isTextAvatar(sourceType) { return [AvatarSource.INITIALS, AvatarSource.VALUE].includes(sourceType); } buildSourceKey(source) { return source.sourceType + '-' + source.sourceId; } sourceHasFailedBefore(source) { return this.failedSources.has(this.buildSourceKey(source)); } markSourceAsFailed(source) { this.failedSources.set(this.buildSourceKey(source), source); } overrideAvatarSources() { this.avatarSources = this.avatarConfigService.getAvatarSources(defaultSources); } overrideAvatarColors() { this.avatarColors = this.avatarConfigService.getAvatarColors(defaultColors); } calculateAsciiCode(value) { return value .split('') .map(letter => letter.charCodeAt(0)) .reduce((previous, current) => previous + current); } getSourcePriority(sourceType) { return this.avatarSources.indexOf(sourceType); } } AvatarService.decorators = [ { type: Injectable } ]; AvatarService.ctorParameters = () => [ { type: HttpClient }, { type: AvatarConfigService } ]; /** * Universal avatar component that * generates avatar from different sources * * export * class AvatarComponent * implements {OnChanges} */ class AvatarComponent { constructor(sourceFactory, avatarService) { this.sourceFactory = sourceFactory; this.avatarService = avatarService; this.round = true; this.size = 50; this.textSizeRatio = 3; this.fgColor = '#FFF'; this.style = {}; this.cornerRadius = 0; this.initialsSize = 0; this.clickOnAvatar = new EventEmitter(); this.isAlive = true; this.avatarSrc = null; this.avatarText = null; this.avatarStyle = {}; this.hostStyle = {}; this.currentIndex = -1; this.sources = []; } onAvatarClicked() { this.clickOnAvatar.emit(this.sources[this.currentIndex]); } /** * Detect inputs change * * param {{ [propKey: string]: SimpleChange }} changes * * memberof AvatarComponent */ ngOnChanges(changes) { for (const propName in changes) { if (this.avatarService.isSource(propName)) { const sourceType = AvatarSource[propName.toUpperCase()]; const currentValue = changes[propName].currentValue; if (currentValue && typeof currentValue === 'string') { this.addSource(sourceType, currentValue); } else { this.removeSource(sourceType); } } } // reinitialize the avatar component when a source property value has changed // the fallback system must be re-invoked with the new values. this.initializeAvatar(); } /** * Fetch avatar source * * memberOf AvatarComponent */ fetchAvatarSource() { const previousSource = this.sources[this.currentIndex]; if (previousSource) { this.avatarService.markSourceAsFailed(previousSource); } const source = this.findNextSource(); if (!source) { return; } if (this.avatarService.isTextAvatar(source.sourceType)) { this.buildTextAvatar(source); this.avatarSrc = null; } else { this.buildImageAvatar(source); } } findNextSource() { while (++this.currentIndex < this.sources.length) { const source = this.sources[this.currentIndex]; if (source && !this.avatarService.sourceHasFailedBefore(source)) { return source; } } return null; } ngOnDestroy() { this.isAlive = false; } /** * Initialize the avatar component and its fallback system */ initializeAvatar() { this.currentIndex = -1; if (this.sources.length > 0) { this.sortAvatarSources(); this.fetchAvatarSource(); this.hostStyle = { width: this.size + 'px', height: this.size + 'px' }; } } sortAvatarSources() { this.sources.sort((source1, source2) => this.avatarService.compareSources(source1.sourceType, source2.sourceType)); } buildTextAvatar(avatarSource) { this.avatarText = avatarSource.getAvatar(+this.initialsSize); this.avatarStyle = this.getInitialsStyle(avatarSource.sourceId); } buildImageAvatar(avatarSource) { this.avatarStyle = this.getImageStyle(); if (avatarSource instanceof AsyncSource) { this.fetchAndProcessAsyncAvatar(avatarSource); } else { this.avatarSrc = avatarSource.getAvatar(+this.size); } } /** * * returns initials style * * memberOf AvatarComponent */ getInitialsStyle(avatarValue) { return Object.assign({ textAlign: 'center', borderRadius: this.round ? '100%' : this.cornerRadius + 'px', border: this.borderColor ? '1px solid ' + this.borderColor : '', textTransform: 'uppercase', color: this.fgColor, backgroundColor: this.bgColor ? this.bgColor : this.avatarService.getRandomColor(avatarValue), font: Math.floor(+this.size / this.textSizeRatio) + 'px Helvetica, Arial, sans-serif', lineHeight: this.size + 'px' }, this.style); } /** * * returns image style * * memberOf AvatarComponent */ getImageStyle() { return Object.assign({ maxWidth: '100%', borderRadius: this.round ? '50%' : this.cornerRadius + 'px', border: this.borderColor ? '1px solid ' + this.borderColor : '', width: this.size + 'px', height: this.size + 'px' }, this.style); } /** * Fetch avatar image asynchronously. * * param {Source} source represents avatar source * memberof AvatarComponent */ fetchAndProcessAsyncAvatar(source) { if (this.avatarService.sourceHasFailedBefore(source)) { return; } this.avatarService .fetchAvatar(source.getAvatar(+this.size)) .pipe(takeWhile(() => this.isAlive), map(response => source.processResponse(response, +this.size))) .subscribe(avatarSrc => (this.avatarSrc = avatarSrc), err => { this.fetchAvatarSource(); }); } /** * Add avatar source * * param sourceType avatar source type e.g facebook,twitter, etc. * param sourceValue source value e.g facebookId value, etc. */ addSource(sourceType, sourceValue) { const source = this.sources.find(s => s.sourceType === sourceType); if (source) { source.sourceId = sourceValue; } else { this.sources.push(this.sourceFactory.newInstance(sourceType, sourceValue)); } } /** * Remove avatar source * * param sourceType avatar source type e.g facebook,twitter, etc. */ removeSource(sourceType) { this.sources = this.sources.filter(source => source.sourceType !== sourceType); } } AvatarComponent.decorators = [ { type: Component, args: [{ // tslint:disable-next-line:component-selector selector: 'ngx-avatar', template: ` <div (click)="onAvatarClicked()" class="avatar-container" [ngStyle]="hostStyle" > <img *ngIf="avatarSrc; else textAvatar" [src]="avatarSrc" [width]="size" [height]="size" [ngStyle]="avatarStyle" (error)="fetchAvatarSource()" class="avatar-content" loading="lazy" /> <ng-template #textAvatar> <div *ngIf="avatarText" class="avatar-content" [ngStyle]="avatarStyle"> {{ avatarText }} </div> </ng-template> </div> `, styles: [` :host { border-radius: 50%; } `] },] } ]; AvatarComponent.ctorParameters = () => [ { type: SourceFactory }, { type: AvatarService } ]; AvatarComponent.propDecorators = { round: [{ type: Input }], size: [{ type: Input }], textSizeRatio: [{ type: Input }], bgColor: [{ type: Input }], fgColor: [{ type: Input }], borderColor: [{ type: Input }], style: [{ type: Input }], cornerRadius: [{ type: Input }], facebook: [{ type: Input, args: ['facebookId',] }], twitter: [{ type: Input, args: ['twitterId',] }], google: [{ type: Input, args: ['googleId',] }], instagram: [{ type: Input, args: ['instagramId',] }], vkontakte: [{ type: Input, args: ['vkontakteId',] }], skype: [{ type: Input, args: ['skypeId',] }], gravatar: [{ type: Input, args: ['gravatarId',] }], github: [{ type: Input, args: ['githubId',] }], custom: [{ type: Input, args: ['src',] }], initials: [{ type: Input, args: ['name',] }], value: [{ type: Input }], placeholder: [{ type: Input }], initialsSize: [{ type: Input }], clickOnAvatar: [{ type: Output }] }; class AvatarModule { static forRoot(avatarConfig) { return { ngModule: AvatarModule, providers: [ { provide: AVATAR_CONFIG, useValue: avatarConfig ? avatarConfig : {} } ] }; } } AvatarModule.decorators = [ { type: NgModule, args: [{ imports: [CommonModule], declarations: [AvatarComponent], providers: [SourceFactory, AvatarService, AvatarConfigService], exports: [AvatarComponent] },] } ]; /* * Public API Surface of ngx-avatar */ /** * Generated bundle index. Do not edit. */ export { AvatarComponent, AvatarModule, AvatarService, AvatarSource, defaultColors, defaultSources, SourceFactory as ɵa, AvatarConfigService as ɵb, AVATAR_CONFIG as ɵc }; //# sourceMappingURL=ngx-avatar.js.map