1/** 2 * <avatar-icon> Web Component 3 * ---------------------------- 4 * A reusable avatar component that displays either a user image or fallback initials. 5 * 6 * USAGE: 7 * <avatar-icon 8 * src="/users/pic/jane-150.jpg" //Optional -- full path, e.g. "/users/pic/{avatar_150}" or "/event-pics/{guest image}" 9 * first-name="Jane" //Mandatory 10 * last-name="Doe" //Mandatory 11 * size="32" //Optional, defaults to 32 (px) 12 * class="mr1" //Optional 13 * > 14 * </avatar-icon> 15 * 16 * BEHAVIOR: 17 * - If the `src` attribute is provided, the component tries to load the image from that path directly. 18 * - If the image fails to load or `src` is not provided, it falls back to rendering the user's initials inside a circle. 19 * - The initials are derived from the `first-name` and `last-name` attributes (e.g., Jane Doe â "JD"). 20 * 21 * ATTRIBUTES: 22 * - src (optional): Full path to the avatar image (e.g. "/users/pic/avatar-150.jpg") 23 * - first-name (required for fallback): The person's first name 24 * - last-name (required for fallback): The person's last name 25 * - size (optional): Diameter in pixels for both the image and the initials circle. Defaults to 32. 26 * - class (optional): CSS classes passed to the internal avatar element (image or initials container) 27 * 28 * STYLES: 29 * - avatar-icon: class applied to <img> element when using an image 30 * - avatar-container: fallback container for initials 31 * - avatar-initials: styled text for the initials 32 * 33 * NOTES: 34 * - External CSS classes added to <avatar-icon> are applied to the internal root element (either <img> or the fallback <div>). 35 * - The component uses Shadow DOM to encapsulate styles. 36 */ 37 38class AvatarIcon extends HTMLElement { 39 constructor() { 40 super(); 41 this.attachShadow({ mode: 'open' }); 42 43 //Base CSS 44 const baseCss2017 = document.createElement('link'); 45 baseCss2017.rel = 'stylesheet'; 46 baseCss2017.href = '/css/base-2017-05-15.css'; 47 this.shadowRoot.appendChild(baseCss2017); 48 49 const baseCss2018 = document.createElement('link'); 50 baseCss2018.rel = 'stylesheet'; 51 baseCss2018.href = '/css/base-2018-11-17.css'; 52 this.shadowRoot.appendChild(baseCss2018); 53 54 // Diameter drives both the image/container box and the initials font size (kept at the 55 // original 14/32 ratio) so smaller avatars (e.g. an 18px pill avatar) still read cleanly. 56 const size = parseInt(this.getAttribute('size'), 10) || 32; 57 const initialsFontSize = Math.round(size * (14 / 32)); 58 59 //Component styling 60 const style = document.createElement('style'); 61 style.textContent = ` 62 .avatar-icon { 63 border-radius: 50%; 64 height: ${size}px; 65 width: ${size}px; 66 object-fit: cover; 67 display: inline-block; 68 vertical-align: middle; 69 } 70 71 .avatar-container { 72 display: inline-flex; 73 align-items: center; 74 justify-content: center; 75 border-radius: 50%; 76 background-color: white; 77 margin-right: 4px; 78 border: 1.75px solid #484848; 79 box-sizing: border-box; 80 height: ${size}px; 81 width: ${size}px; 82 overflow: hidden; 83 text-align: center; 84 } 85 86 .avatar-initials { 87 color: #222; 88 font-weight: 700; 89 user-select: none; 90 font-size: ${initialsFontSize}px; 91 line-height: 1; 92 text-transform: uppercase; 93 } 94 `; 95 this.shadowRoot.appendChild(style); 96 97 //Render avatar 98 const avatarElement = this.createAvatarIcon(); 99 this.shadowRoot.appendChild(avatarElement); 100 } 101 102 createAvatarIcon() { 103 const relativePath = this.getAttribute('src'); 104 const src = relativePath 105 ? (relativePath.startsWith('/') ? relativePath : `/users/pic/${relativePath}`) 106 : null; 107 const firstName = this.getAttribute('first-name') || ''; 108 const lastName = this.getAttribute('last-name') || ''; 109 const externalClasses = this.getAttribute('class') || ''; 110 const initials = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); 111 112 //Function to create initials fallback 113 const createInitialsFallback = () => { 114 const container = document.createElement('div'); 115 container.className = `avatar-container ${externalClasses}`.trim(); 116 117 const initialsDiv = document.createElement('div'); 118 initialsDiv.classList.add('avatar-initials'); 119 initialsDiv.setAttribute('aria-label', `${firstName}-${lastName}`); 120 initialsDiv.textContent = initials || ''; 121 122 container.appendChild(initialsDiv); 123 return container; 124 }; 125 126 if(src) { 127 const avatarIcon = document.createElement('img'); 128 avatarIcon.src = src; 129 avatarIcon.alt = `${firstName || 'User'}'s icon`; 130 avatarIcon.className = `avatar-icon ${externalClasses}`.trim(); 131 132 //Fallback if image fails to load 133 avatarIcon.onerror = () => { 134 //Remove the broken image and replace it with initials 135 const fallback = createInitialsFallback(); 136 if(this.shadowRoot.contains(avatarIcon)) { 137 this.shadowRoot.replaceChild(fallback, avatarIcon); 138 } 139 }; 140 141 return avatarIcon; 142 } else { 143 return createInitialsFallback(); 144 } 145 } 146} 147 148customElements.define('avatar-icon', AvatarIcon);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.