PageSourceSearch

https://www.mecourse.com/scripts/references-info-popup.js

js mecourse.com collected 2026-09-28 08:16:15 UTC 9,430 bytes, 299 lines download raw bytes

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">&times;</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.