1/** 2 * Author kickout â fixed-position hover panel (avatar + full name + org). 3 * Replaces native title tooltips on truncated author chips in card/list views. 4 */ 5(function initAuthorKickout() { 6 var portal = null; 7 var hideTimer = null; 8 var activeTrigger = null; 9 10 function escapeHtml(str) { 11 return String(str || '') 12 .replace(/&/g, '&') 13 .replace(/</g, '<') 14 .replace(/>/g, '>') 15 .replace(/"/g, '"'); 16 } 17 18 function escapeAttr(str) { 19 return escapeHtml(str).replace(/'/g, '''); 20 } 21 22 function truthy(val) { 23 return val === '1' || val === 'true' || val === true; 24 } 25 26 function ensurePortal() { 27 if (portal) return portal; 28 portal = document.createElement('div'); 29 portal.id = 'cdp-author-kickout-portal'; 30 portal.className = 'cdp-author-kickout'; 31 portal.setAttribute('role', 'tooltip'); 32 portal.innerHTML = '<div class="cdp-author-kickout__panel"></div>'; 33 portal.addEventListener('mouseenter', function () { 34 clearTimeout(hideTimer); 35 }); 36 portal.addEventListener('mouseleave', function (e) { 37 var related = e.relatedTarget; 38 if (related && activeTrigger && activeTrigger.contains(related)) return; 39 hideTimer = setTimeout(hideKickout, 120); 40 }); 41 document.body.appendChild(portal); 42 return portal; 43 } 44 45 function buildAvatarHtml(pic, initials, color, showAvatar) { 46 if (!truthy(showAvatar)) return ''; 47 if (pic) { 48 return '<img src="' + escapeAttr(pic) + '" class="cdp-author-kickout__avatar cdp-author-kickout__avatar--img" alt="">'; 49 } 50 if (initials) { 51 return '<span class="cdp-author-kickout__avatar cdp-author-kickout__avatar--initials" style="background:' + escapeAttr(color || '#1e40af') + '">' + escapeHtml(initials) + '</span>'; 52 } 53 return ''; 54 } 55 56 function buildProfileLinkHtml(profileUrl) { 57 if (!profileUrl) return ''; 58 return '<a href="' + escapeAttr(profileUrl) + '" class="cdp-author-kickout__profile-link" target="_blank" rel="noopener noreferrer">View profile</a>'; 59 } 60 61 function buildRow(name, company, pic, initials, color, showAvatar, showCompany, profileUrl) { 62 var avatar = buildAvatarHtml(pic, initials, color, showAvatar); 63 var companyHtml = truthy(showCompany) && company 64 ? '<span class="cdp-author-kickout__company">' + escapeHtml(company) + '</span>' 65 : ''; 66 var profileLinkHtml = buildProfileLinkHtml(profileUrl); 67 return '<div class="cdp-author-kickout__row">' + 68 (avatar ? '<div class="cdp-author-kickout__avatar-wrap">' + avatar + '</div>' : '') + 69 '<div class="cdp-author-kickout__text">' + 70 '<span class="cdp-author-kickout__name">' + escapeHtml(name) + '</span>' + 71 companyHtml + 72 profileLinkHtml + 73 '</div></div>'; 74 } 75 76 function renderPanel(trigger) { 77 var panel = ensurePortal().querySelector('.cdp-author-kickout__panel'); 78 var html = ''; 79 80 if (trigger.getAttribute('data-kickout-multi') === '1') { 81 var raw = trigger.getAttribute('data-kickout-authors') || '[]'; 82 try { 83 var authors = JSON.parse(raw); 84 if (Array.isArray(authors)) { 85 authors.forEach(function (a) { 86 html += buildRow( 87 a.name || '', 88 a.company || '', 89 a.pic || '', 90 a.initials || '', 91 a.color || '#1e40af', 92 a.showAvatar, 93 a.showCompany, 94 a.profileUrl || '' 95 ); 96 }); 97 } 98 } catch (e) { /* ignore malformed JSON */ } 99 } else { 100 html = buildRow( 101 trigger.getAttribute('data-kickout-name') || '', 102 trigger.getAttribute('data-kickout-company') || '', 103 trigger.getAttribute('data-kickout-pic') || '', 104 trigger.getAttribute('data-kickout-initials') || '', 105 trigger.getAttribute('data-kickout-color') || '#1e40af', 106 trigger.getAttribute('data-kickout-show-avatar'), 107 trigger.getAttribute('data-kickout-show-company'), 108 trigger.getAttribute('data-kickout-profile-url') || '' 109 ); 110 } 111 112 panel.innerHTML = html; 113 return panel.innerHTML.length > 0; 114 } 115 116 function positionPortal(trigger) { 117 var el = ensurePortal(); 118 var rect = trigger.getBoundingClientRect(); 119 el.style.visibility = 'hidden'; 120 el.classList.add('is-visible'); 121 var pRect = el.getBoundingClientRect(); 122 var gap = 8; 123 var left = rect.left + (rect.width / 2) - (pRect.width / 2); 124 var top = rect.top - pRect.height - gap; 125 if (top < gap) { 126 top = rect.bottom + gap; 127 } 128 left = Math.max(gap, Math.min(left, window.innerWidth - pRect.width - gap)); 129 el.style.left = left + 'px'; 130 el.style.top = top + 'px'; 131 el.style.visibility = 'visible'; 132 } 133 134 function showKickout(trigger) { 135 if (!renderPanel(trigger)) return; 136 positionPortal(trigger); 137 activeTrigger = trigger; 138 } 139 140 function hideKickout() { 141 if (portal) { 142 portal.classList.remove('is-visible'); 143 portal.style.visibility = 'hidden'; 144 } 145 activeTrigger = null; 146 } 147 148 document.addEventListener('mouseover', function (e) { 149 var trigger = e.target.closest('[data-author-kickout-trigger]'); 150 if (!trigger) return; 151 clearTimeout(hideTimer); 152 if (activeTrigger !== trigger) { 153 showKickout(trigger); 154 } 155 }); 156 157 document.addEventListener('mouseout', function (e) { 158 var trigger = e.target.closest('[data-author-kickout-trigger]'); 159 if (!trigger || trigger !== activeTrigger) return; 160 var related = e.relatedTarget; 161 if (related && (trigger.contains(related) || (portal && portal.contains(related)))) return; 162 hideTimer = setTimeout(hideKickout, 120); 163 }); 164 165 window.addEventListener('scroll', hideKickout, true); 166 window.addEventListener('resize', hideKickout); 167})();
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.