1// /public/scripts/references-info-popup.js 2// 3// References info popup only 4// - DOES NOT handle the main references toggle 5// - Adds a non-modal body-level popup for the .refs-info-btn button 6// - Safe alongside the existing "Accessible References & Key Points UI" script 7// - Depends on jQuery 8 9(function () { 10 'use strict'; 11 12 if (!window.jQuery) { 13 console.warn('[refs-info-popup] jQuery not found. This script requires jQuery.'); 14 return; 15 } 16 17 var $ = window.jQuery; 18 19 var STYLE_ID = 'refs-info-popup-style'; 20 var POPUP_ID = 'refs-explainer'; 21 var POPUP_TITLE_ID = 'refs-explainer-title'; 22 var POPUP_CONTENT_ID = 'refs-explainer-content'; 23 24 var INFO_BUTTON_SELECTOR = '.refs-info-btn'; 25 var TEMPLATE_SELECTOR = '#refsExplainerTemplate'; 26 var REFERENCES_TOGGLE_SELECTOR = '#referencesToggle'; 27 28 var $popup = null; 29 var $currentInfoBtn = null; 30 31 var FALLBACK_HTML = 32 '<p>' + 33 'References are included to demonstrate that all the content in this course is ' + 34 'rigorously evidence-based, and has been prepared using trusted and authoritative sources.' + 35 '</p>' + 36 '<p>' + 37 'They also serve as starting points for further reading and deeper exploration at your own pace.' + 38 '</p>'; 39 40 $(function () { 41 injectStylesOnce(); 42 ensureRefsExplainer(); 43 bindEvents(); 44 }); 45 46 function injectStylesOnce() { 47 if (document.getElementById(STYLE_ID)) return; 48 49 var css = 50 '.refs-info-btn{' + 51 '-webkit-appearance:none;appearance:none;background:none;border:0;padding:0;margin:0 0 0 6px;' + 52 'color:#555;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;' + 53 'vertical-align:baseline;line-height:1;' + 54 '}' + 55 '.refs-info-btn:hover{color:#111;}' + 56 '.refs-info-btn[aria-expanded="true"]{color:#111;}' + 57 '.refs-info-btn:focus-visible,.refs-explainer-close:focus-visible,.refs-explainer-content a:focus-visible{' + 58 'outline:2px solid currentColor;outline-offset:2px;' + 59 '}' + 60 '.refs-info-icon{' + 61 'display:inline-flex;align-items:center;justify-content:center;width:1.15em;height:1.15em;' + 62 'border:1px solid currentColor;border-radius:999px;font-size:.78em;font-weight:700;line-height:1;' + 63 '}' + 64 '.refs-explainer[hidden]{display:none !important;}' + 65 '.refs-explainer{' + 66 'position:fixed;z-index:var(--z-popover, 8500);width:min(420px,calc(100vw - 16px));max-width:min(420px,calc(100vw - 16px));' + 67 'border:1px solid #ccc;background:#fff;color:#333;box-shadow:0 2px 10px rgba(0,0,0,.18);' + 68 '}' + 69 '.refs-explainer-inner{padding:10px 12px 12px;}' + 70 '.refs-explainer-header{' + 71 'display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:6px;' + 72 '}' + 73 '.refs-explainer-title{margin:0;font-size:13px;line-height:1.3;font-weight:600;}' + 74 '.refs-explainer-close{' + 75 '-webkit-appearance:none;appearance:none;background:none;border:0;padding:0;margin:0;' + 76 'font:inherit;font-size:20px;line-height:1;color:#555;cursor:pointer;' + 77 '}' + 78 '.refs-explainer-content{' + 79 'font-size:13px;line-height:1.45;white-space:normal;overflow-wrap:anywhere;word-break:break-word;' + 80 '}' + 81 '.refs-explainer-content p{margin:0 0 8px 0;}' + 82 '.refs-explainer-content p:last-child{margin-bottom:0;}'; 83 84 var style = document.createElement('style'); 85 style.id = STYLE_ID; 86 style.type = 'text/css'; 87 style.appendChild(document.createTextNode(css)); 88 document.head.appendChild(style); 89 } 90 91 function ensureRefsExplainer() { 92 if ($popup && $popup.length) return $popup; 93 94 $popup = $('#' + POPUP_ID); 95 if ($popup.length) return $popup; 96 97 $popup = $( 98 '<div ' + 99 'class="refs-explainer" ' + 100 'id="' + POPUP_ID + '" ' + 101 'role="dialog" ' + 102 'aria-modal="false" ' + 103 'aria-hidden="true" ' + 104 'aria-labelledby="' + POPUP_TITLE_ID + '" ' + 105 'aria-describedby="' + POPUP_CONTENT_ID + '" ' + 106 'hidden>' + 107 '<div class="refs-explainer-inner">' + 108 '<div class="refs-explainer-header">' + 109 '<h2 class="refs-explainer-title" id="' + POPUP_TITLE_ID + '">Why we include references</h2>' + 110 '<button class="refs-explainer-close" type="button" aria-label="Close explanation">×</button>' + 111 '</div>' + 112 '<div class="refs-explainer-content" id="' + POPUP_CONTENT_ID + '"></div>' + 113 '</div>' + 114 '</div>' 115 ).appendTo(document.body); 116 117 $popup.on('click', '.refs-explainer-close', function (e) { 118 e.preventDefault(); 119 hideRefsExplainer(true); 120 }); 121 122 return $popup; 123 } 124 125 function getExplainerHtml() { 126 var $template = $(TEMPLATE_SELECTOR); 127 if ($template.length) { 128 var html = $.trim($template.html()); 129 if (html) return html; 130 } 131 return FALLBACK_HTML; 132 } 133 134 function positionRefsExplainerNear($target) { 135 if (!$popup || !$popup.length || !$target || !$target.length) return; 136 137 var rect = $target[0].getBoundingClientRect(); 138 var popupW = $popup.outerWidth(); 139 var popupH = $popup.outerHeight(); 140 var vw = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); 141 var vh = Math.max(document.documentElement.clientHeight, window.innerHeight || 0
141); 142 143 var margin = 8; 144 var gap = 12; 145 146 var left = rect.left + (rect.width / 2) - (popupW / 2); 147 var top = rect.top - popupH - gap; 148 var placement = 'top'; 149 150 left = Math.max(margin, Math.min(left, vw - popupW - margin)); 151 152 if (top < margin) { 153 top = rect.bottom + gap; 154 placement = 'bottom'; 155 } 156 157 if (top + popupH > vh - margin) { 158 top = Math.max(margin, vh - popupH - margin); 159 } 160 161 $popup.css({ 162 left: Math.round(left) + 'px', 163 top: Math.round(top) + 'px' 164 }).attr('data-popper-placement', placement); 165 } 166 167 function showRefsExplainer($btn) { 168 ensureRefsExplainer(); 169 170 if ($currentInfoBtn && $currentInfoBtn.length && $currentInfoBtn.get(0) !== $btn.get(0)) { 171 $currentInfoBtn.attr('aria-expanded', 'false'); 172 } 173 174 $popup.find('.refs-explainer-content').html(getExplainerHtml()); 175 176 $currentInfoBtn = $btn; 177 $currentInfoBtn.attr('aria-expanded', 'true'); 178 179 $popup 180 .prop('hidden', false) 181 .attr('aria-hidden', 'false') 182 .css({ 183 display: 'block', 184 visibility: 'hidden', 185 left: '-9999px', 186 top: '0px' 187 }); 188 189 positionRefsExplainerNear($btn); 190 191 $popup.css('visibility', 'visible'); 192 193 var closeBtn = $popup.find('.refs-explainer-close').get(0); 194 if (closeBtn) closeBtn.focus(); 195 } 196 197 function hideRefsExplainer(returnFocus) { 198 if (!$popup || !$popup.length) return; 199 200 var btnToFocus = $currentInfoBtn; 201 202 $popup 203 .prop('hidden', true) 204 .attr('aria-hidden', 'true') 205 .css({ 206 visibility: '', 207 left: '', 208 top: '', 209 display: '' 210 }) 211 .removeAttr('data-popper-placement'); 212 213 if ($currentInfoBtn && $currentInfoBtn.length) { 214 $currentInfoBtn.attr('aria-expanded', 'false'); 215 } 216 217 $currentInfoBtn = null; 218 219 if ( 220 returnFocus && 221 btnToFocus && 222 btnToFocus.length && 223 typeof btnToFocus.get(0).focus === 'function' 224 ) { 225 btnToFocus.get(0).focus(); 226 } 227 } 228 229 function toggleRefsExplainer($btn) { 230 if (!$btn || !$btn.length) return; 231 232 if ($popup && !$popup.prop('hidden') && $currentInfoBtn && $currentInfoBtn.get(0) === $btn.get(0)) { 233 hideRefsExplainer(false); 234 return; 235 } 236 237 showRefsExplainer($btn); 238 } 239 240 function bindEvents() { 241 // Info button only 242 $(document).on('click.refsInfoPopup', INFO_BUTTON_SELECTOR, function (e) { 243 e.preventDefault(); 244 e.stopPropagation(); 245 toggleRefsExplainer($(this)); 246 }); 247 248 // Allow interaction inside popup 249 $(document).on('click.refsInfoPopup', '#' + POPUP_ID, function (e) { 250 e.stopPropagation(); 251 }); 252 253 // Close if main References panel is toggled 254 $(document).on('click.refsInfoPopup', REFERENCES_TOGGLE_SELECTOR, function () { 255 hideRefsExplainer(false); 256 }); 257 258 // Outside click closes 259 $(document).on('click.refsInfoPopup', function (e) { 260 if (!$(e.target).closest('#' + POPUP_ID + ', ' + INFO_BUTTON_SELECTOR).length) { 261 hideRefsExplainer(false); 262 } 263 }); 264 265 // Escape closes 266 $(document).on('keydown.refsInfoPopup', function (e) { 267 if (e.key === 'Escape' && $popup && !$popup.prop('hidden')) { 268 e.preventDefault(); 269 hideRefsExplainer(true); 270 } 271 }); 272 273 // Focus moving away closes 274 $(document).on('focusin.refsInfoPopup', function (e) { 275 if (!$popup || $popup.prop('hidden')) return; 276 277 var insidePopup = $(e.target).closest('#' + POPUP_ID).length > 0; 278 var onLauncher = $currentInfoBtn && $currentInfoBtn.length && e.target === $currentInfoBtn.get(0); 279 280 if (!insidePopup && !onLauncher) { 281 hideRefsExplainer(false); 282 } 283 }); 284 285 // Hide on any scroll, including nested scroll containers 286 document.addEventListener('scroll', function () { 287 if ($popup && !$popup.prop('hidden')) { 288 hideRefsExplainer(false); 289 } 290 }, true); 291 292 // Hide on resize 293 window.addEventListener('resize', function () { 294 if ($popup && !$popup.prop('hidden')) { 295 hideRefsExplainer(false); 296 } 297 }); 298 } 299})();
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.