@rathnasgala/theme 0.0.20 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,106 @@
1
1
  function selectableFallback(control) {
2
2
  const region = control.closest('.gala-share');
3
3
  const fallback = region?.querySelector('.gala-share__fallback');
4
+ fallback?.classList.add('gala-share__fallback--visible');
4
5
  fallback?.focus();
5
6
  fallback?.select();
6
7
  const status = region?.querySelector('.gala-share__status');
7
8
  if (status) status.textContent = 'Select and copy the URL shown.';
8
9
  }
9
10
 
11
+ const readingProgress = document.querySelector('[data-reading-progress]');
12
+ const readingContent = document.querySelector('.gala-markdown');
13
+ if (readingProgress && readingContent) {
14
+ let progressFrame = null;
15
+ const updateReadingProgress = () => {
16
+ progressFrame = null;
17
+ const start = readingContent.offsetTop;
18
+ const distance = Math.max(1, readingContent.offsetHeight - window.innerHeight);
19
+ const progress = Math.min(1, Math.max(0, (window.scrollY - start) / distance));
20
+ readingProgress.value = progress;
21
+ };
22
+ const scheduleReadingProgress = () => {
23
+ if (progressFrame == null) progressFrame = requestAnimationFrame(updateReadingProgress);
24
+ };
25
+ addEventListener('scroll', scheduleReadingProgress, { passive: true });
26
+ addEventListener('resize', scheduleReadingProgress, { passive: true });
27
+ scheduleReadingProgress();
28
+ }
29
+
30
+ const articleToc = document.querySelector('.gala-toc');
31
+ if (articleToc?.tagName === 'DETAILS') {
32
+ const tocNavigation = articleToc.querySelector('nav');
33
+ const tocLinks = [...articleToc.querySelectorAll('a[href^="#"]')];
34
+ const headings = tocLinks.map((link) => document.getElementById(link.hash.slice(1))).filter(Boolean);
35
+ let tocFrame = null;
36
+ const synchronizeToc = () => {
37
+ tocFrame = null;
38
+ articleToc.classList.toggle('gala-toc--floating', articleToc.getBoundingClientRect().top <= 96);
39
+ if (!headings.length) return;
40
+ const readingLine = window.innerHeight * 0.42;
41
+ let active = headings[0];
42
+ for (const heading of headings) {
43
+ if (heading.getBoundingClientRect().top <= readingLine) active = heading;
44
+ else break;
45
+ }
46
+ const activeLink = tocLinks.find((link) => link.hash === `#${active.id}`);
47
+ for (const link of tocLinks) {
48
+ if (link === activeLink) link.setAttribute('aria-current', 'location');
49
+ else link.removeAttribute('aria-current');
50
+ }
51
+ if (tocNavigation && tocLinks.length) {
52
+ const firstRect = tocLinks[0].getBoundingClientRect();
53
+ const lastRect = tocLinks[tocLinks.length - 1].getBoundingClientRect();
54
+ articleToc.classList.toggle('gala-toc--overflowing',
55
+ lastRect.bottom - firstRect.top > tocNavigation.clientHeight);
56
+ }
57
+ if (activeLink && tocNavigation && tocNavigation.scrollHeight > tocNavigation.clientHeight) {
58
+ const navigationRect = tocNavigation.getBoundingClientRect();
59
+ const activeRect = activeLink.getBoundingClientRect();
60
+ const target = tocNavigation.scrollTop + activeRect.top - navigationRect.top
61
+ - (tocNavigation.clientHeight - activeRect.height) / 2;
62
+ tocNavigation.scrollTo({ top: Math.max(0, target), behavior: 'auto' });
63
+ }
64
+ };
65
+ const scheduleToc = () => {
66
+ if (tocFrame == null) tocFrame = requestAnimationFrame(synchronizeToc);
67
+ };
68
+ addEventListener('scroll', scheduleToc, { passive: true });
69
+ addEventListener('resize', scheduleToc, { passive: true });
70
+ scheduleToc();
71
+ }
72
+
73
+ const actionRail = document.querySelector('.gala-action-rail');
74
+ const actionDock = document.querySelector('[data-action-dock]');
75
+ if (actionRail && actionDock) {
76
+ const synchronizeActionRail = (entries) => {
77
+ actionRail.classList.toggle('gala-action-rail--integrated', entries[0].isIntersecting);
78
+ };
79
+ new IntersectionObserver(synchronizeActionRail, {
80
+ rootMargin: '0px 0px 18% 0px', threshold: 0
81
+ }).observe(actionDock);
82
+ }
83
+
84
+ function presentFollowState(control, following) {
85
+ const label = following ? 'Unfollow article' : 'Follow article';
86
+ control.setAttribute('aria-pressed', String(following));
87
+ control.setAttribute('aria-label', label);
88
+ control.title = label;
89
+ const visibleLabel = control.querySelector('[data-follow-label]');
90
+ if (visibleLabel) visibleLabel.textContent = label;
91
+ }
92
+
10
93
  document.addEventListener('click', async (event) => {
94
+ if (event.target instanceof HTMLDialogElement && event.target.open) {
95
+ const bounds = event.target.getBoundingClientRect();
96
+ const outside = event.clientX < bounds.left || event.clientX > bounds.right
97
+ || event.clientY < bounds.top || event.clientY > bounds.bottom;
98
+ if (outside) {
99
+ event.target.close();
100
+ return;
101
+ }
102
+ }
103
+
11
104
  const embed = event.target.closest('[data-gala-embed-load]');
12
105
  if (embed) {
13
106
  const container = embed.closest('[data-gala-embed]');
@@ -65,8 +158,7 @@ document.addEventListener('click', async (event) => {
65
158
  targetType: 'articles', targetId: region.dataset.articleId
66
159
  });
67
160
  if (!saved) return;
68
- follow.setAttribute('aria-pressed', String(active));
69
- follow.textContent = active ? 'Unfollow article' : 'Follow article';
161
+ presentFollowState(follow, active);
70
162
  return;
71
163
  }
72
164
 
@@ -87,6 +179,20 @@ document.addEventListener('click', async (event) => {
87
179
  return;
88
180
  }
89
181
 
182
+ const nativeShare = event.target.closest('[data-native-share]');
183
+ if (nativeShare) {
184
+ if (navigator.share) {
185
+ try {
186
+ await navigator.share({ title: document.title, url: nativeShare.dataset.nativeShare });
187
+ } catch (error) {
188
+ if (error.name !== 'AbortError') selectableFallback(nativeShare);
189
+ }
190
+ } else {
191
+ selectableFallback(nativeShare);
192
+ }
193
+ return;
194
+ }
195
+
90
196
  const copy = event.target.closest('[data-copy-code]');
91
197
  if (!copy) return;
92
198
  const code = copy.closest('.gala-code-block')?.querySelector('code');
@@ -115,13 +221,6 @@ codeBlocks.forEach((pre) => {
115
221
  wrapper.prepend(control);
116
222
  });
117
223
 
118
- function textElement(name, text, className) {
119
- const element = document.createElement(name);
120
- element.textContent = text;
121
- if (className) element.className = className;
122
- return element;
123
- }
124
-
125
224
  function engagementCounts(data) {
126
225
  const reactionTotal = Object.values(data.reactions ?? {})
127
226
  .reduce((total, count) => total + (Number.isSafeInteger(count) ? count : 0), 0);
@@ -132,33 +231,22 @@ function engagementCounts(data) {
132
231
  ];
133
232
  }
134
233
 
135
- function renderEngagement(region, payload, appendComments = false) {
234
+ function publicCount(value) {
235
+ if (value < 1000) return '<1K';
236
+ return new Intl.NumberFormat('en', {
237
+ notation: 'compact', maximumFractionDigits: 1
238
+ }).format(value);
239
+ }
240
+
241
+ function renderEngagement(region, payload) {
136
242
  const data = payload?.data;
137
243
  if (!data || typeof data !== 'object') throw new TypeError('Engagement data is invalid');
138
- const live = region.querySelector('[data-engagement-live]');
139
- if (!live) return;
140
- if (!appendComments) live.replaceChildren();
141
-
142
- if (!appendComments && data.profile) {
143
- const profile = document.createElement('section');
144
- profile.className = 'gala-engagement-profile';
145
- profile.setAttribute('aria-label', 'Author profile');
146
- profile.append(textElement('h2', data.profile.displayName));
147
- if (data.profile.username) profile.append(textElement('p', `@${data.profile.username}`));
148
- if (Number.isSafeInteger(data.profile.followerCount)) {
149
- profile.append(textElement('p', `${data.profile.followerCount} followers`));
150
- }
151
- live.append(profile);
152
- }
153
-
154
- if (!appendComments) {
155
- const summary = document.createElement('dl');
156
- for (const [label, value] of engagementCounts(data)) {
157
- const item = document.createElement('div');
158
- item.append(textElement('dt', label), textElement('dd', String(value)));
159
- summary.append(item);
160
- }
161
- live.append(summary);
244
+ for (const [label, value] of engagementCounts(data)) {
245
+ region.querySelectorAll(`[data-engagement-stat="${label.toLowerCase()}"]`)
246
+ .forEach((count) => {
247
+ count.textContent = publicCount(value);
248
+ count.closest('div')?.setAttribute('title', `${value} ${label.toLowerCase()}`);
249
+ });
162
250
  }
163
251
 
164
252
  region.querySelector('[data-engagement-snapshot]')?.remove();
@@ -167,6 +255,7 @@ function renderEngagement(region, payload, appendComments = false) {
167
255
 
168
256
  async function refreshEngagement(region, commentsCursor = '', appendComments = false, fresh = false) {
169
257
  const status = region.querySelector('[data-engagement-status]');
258
+ const commentsOwnStatus = Boolean(region.querySelector('[data-gala-comments]'));
170
259
  try {
171
260
  const requestUrl = new URL(region.dataset.engagementUrl);
172
261
  if (commentsCursor) requestUrl.searchParams.set('commentsCursor', commentsCursor);
@@ -177,11 +266,11 @@ async function refreshEngagement(region, commentsCursor = '', appendComments = f
177
266
  });
178
267
  if (!response.ok) throw new Error(`Engagement returned HTTP ${response.status}`);
179
268
  const payload = await response.json();
180
- renderEngagement(region, payload, appendComments);
181
- if (status) status.textContent = payload.errors?.length
269
+ renderEngagement(region, payload);
270
+ if (status && !commentsOwnStatus) status.textContent = payload.errors?.length
182
271
  ? 'Some engagement data is temporarily unavailable.' : '';
183
272
  } catch {
184
- if (status) status.textContent = 'Live engagement data is temporarily unavailable.';
273
+ if (status && !commentsOwnStatus) status.textContent = 'Engagement is temporarily unavailable.';
185
274
  }
186
275
  }
187
276
 
@@ -225,6 +314,7 @@ let sessionUser = null;
225
314
  // The reader the current engagement render was built for: null until the frame reports, and
226
315
  // null again for a signed-out reader, so an anonymous load never re-requests.
227
316
  let renderedSessionUser = null;
317
+ let readerStateRequestId = null;
228
318
  const pendingEngagementWrites = new Map();
229
319
 
230
320
  /**
@@ -331,7 +421,14 @@ function sendEngagementWrite(operation, payload) {
331
421
  if (!sessionFrame || !sessionUser) return Promise.reject(new Error('AUTHENTICATION_REQUIRED'));
332
422
  const requestId = crypto.randomUUID();
333
423
  return new Promise((resolve, reject) => {
334
- pendingEngagementWrites.set(requestId, { resolve, reject });
424
+ const timeout = setTimeout(() => {
425
+ pendingEngagementWrites.delete(requestId);
426
+ reject(new Error('REQUEST_TIMEOUT'));
427
+ }, 10_000);
428
+ pendingEngagementWrites.set(requestId, {
429
+ resolve: (value) => { clearTimeout(timeout); resolve(value); },
430
+ reject: (error) => { clearTimeout(timeout); reject(error); }
431
+ });
335
432
  sessionFrame.contentWindow.postMessage({
336
433
  type: 'gala-engagement-write', requestId, operation, payload
337
434
  }, new URL(sessionFrame.src).origin);
@@ -392,13 +489,26 @@ if (sessionFrame) {
392
489
  else pending.reject(new Error(event.data.error?.code || 'ENGAGEMENT_WRITE_FAILED'));
393
490
  return;
394
491
  }
492
+ if (event.data?.type === 'gala-reader-state-result') {
493
+ if (event.data.requestId !== readerStateRequestId || event.data.ok !== true) return;
494
+ readerStateRequestId = null;
495
+ const active = new Set(Array.isArray(event.data.state?.reactions) ? event.data.state.reactions : []);
496
+ document.querySelectorAll('[data-reaction]').forEach((control) => {
497
+ control.setAttribute('aria-pressed', String(active.has(control.dataset.reaction)));
498
+ });
499
+ document.querySelectorAll('[data-follow-article]').forEach((control) => {
500
+ const following = event.data.state?.following === true;
501
+ presentFollowState(control, following);
502
+ });
503
+ return;
504
+ }
395
505
  /* The frame is cross-origin, so its content height is not readable from here — it reports
396
506
  its own, and the box is sized to it. Without this the account panel scrolled inside a fixed
397
507
  box, clipping the first line of its own text. */
398
508
  if (event.data?.type === 'gala-session-height') {
399
509
  const height = Number(event.data.height);
400
510
  if (Number.isFinite(height) && height > 0 && height < 2000) {
401
- sessionFrame.style.height = `${Math.ceil(height)}px`;
511
+ sessionFrame.height = String(Math.ceil(height));
402
512
  }
403
513
  return;
404
514
  }
@@ -410,12 +520,24 @@ if (sessionFrame) {
410
520
  control.setAttribute('aria-label', displayName
411
521
  ? `Account: ${displayName}` : 'Sign in or view account');
412
522
  control.title = displayName ? `Account: ${displayName}` : 'Account';
523
+ const label = control.querySelector('[data-user-label]');
524
+ if (label) label.textContent = displayName || 'Sign in';
413
525
  }
414
526
  // The session frame reports on every page load, signed in or not. Re-reading engagement
415
527
  // then duplicated the request made on load and returned an identical payload for anyone
416
528
  // who was not signed in. Only a change of reader can change what the API answers.
417
529
  const changed = renderedSessionUser !== (sessionUser?.id ?? null);
418
530
  renderedSessionUser = sessionUser?.id ?? null;
531
+ if (sessionUser) {
532
+ const region = document.querySelector('[data-article-id]');
533
+ if (region) {
534
+ readerStateRequestId = crypto.randomUUID();
535
+ sessionFrame.contentWindow.postMessage({
536
+ type: 'gala-reader-state-request', requestId: readerStateRequestId,
537
+ articleId: region.dataset.articleId
538
+ }, sessionOrigin);
539
+ }
540
+ }
419
541
  document.querySelectorAll('[data-engagement-url]').forEach((region) => {
420
542
  if (changed) refreshEngagement(region);
421
543
  });
@@ -0,0 +1,2 @@
1
+ (()=>{var b=["system","light","dark"],O="gala-color-mode";function D(){let e=document.documentElement.dataset.mode;return b.includes(e)?e:"system"}function J(){try{let e=localStorage.getItem(O);if(b.includes(e))return e}catch{return D()}return D()}function M(e){document.documentElement.dataset.mode=e;let t=b[(b.indexOf(e)+1)%b.length];document.querySelectorAll("[data-theme-mode-toggle]").forEach(n=>{let a=n.querySelector?.("[data-theme-mode-label]");a?a.textContent=`Theme: ${e}`:n.textContent=`Theme: ${e}`,n.setAttribute("aria-label",`Color mode: ${e}. Activate for ${t}.`)})}document.addEventListener("DOMContentLoaded",()=>{M(J()),document.querySelectorAll("[data-theme-mode-toggle]").forEach(e=>{e.addEventListener("click",()=>{let t=b[(b.indexOf(document.documentElement.dataset.mode)+1)%b.length];try{localStorage.setItem(O,t)}catch{}M(t)})})});var F="gala-language-preference";function Z(){try{return localStorage.getItem(F)}catch{return null}}function P(e,t){return[...e.options].find(n=>n.value===t)}document.addEventListener("DOMContentLoaded",()=>{let e=Z();document.querySelectorAll("[data-language-preference]").forEach(t=>{e!=null&&P(t,e)!=null&&(t.value=e),t.addEventListener("change",()=>{try{localStorage.setItem(F,t.value)}catch{}if(!t.hasAttribute("data-navigate-on-selection"))return;let n=P(t,t.value);n?.dataset.url&&window.location.assign(n.dataset.url)})})});function v(e){let t=e.closest(".gala-share"),n=t?.querySelector(".gala-share__fallback");n?.classList.add("gala-share__fallback--visible"),n?.focus(),n?.select();let a=t?.querySelector(".gala-share__status");a&&(a.textContent="Select and copy the URL shown.")}var G=document.querySelector("[data-reading-progress]"),q=document.querySelector(".gala-markdown");if(G&&q){let e=null,t=()=>{e=null;let a=q.offsetTop,s=Math.max(1,q.offsetHeight-window.innerHeight),o=Math.min(1,Math.max(0,(window.scrollY-a)/s));G.value=o},n=()=>{e==null&&(e=requestAnimationFrame(t))};addEventListener("scroll",n,{passive:!0}),addEventListener("resize",n,{passive:!0}),n()}var C=document.querySelector(".gala-toc");if(C?.tagName==="DETAILS"){let e=C.querySelector("nav"),t=[...C.querySelectorAll('a[href^="#"]')],n=t.map(d=>document.getElementById(d.hash.slice(1))).filter(Boolean),a=null,s=()=>{if(a=null,C.classList.toggle("gala-toc--floating",C.getBoundingClientRect().top<=96),!n.length)return;let d=window.innerHeight*.42,f=n[0];for(let i of n)if(i.getBoundingClientRect().top<=d)f=i;else break;let E=t.find(i=>i.hash===`#${f.id}`);for(let i of t)i===E?i.setAttribute("aria-current","location"):i.removeAttribute("aria-current");if(e&&t.length){let i=t[0].getBoundingClientRect(),r=t[t.length-1].getBoundingClientRect();C.classList.toggle("gala-toc--overflowing",r.bottom-i.top>e.clientHeight)}if(E&&e&&e.scrollHeight>e.clientHeight){let i=e.getBoundingClientRect(),r=E.getBoundingClientRect(),c=e.scrollTop+r.top-i.top-(e.clientHeight-r.height)/2;e.scrollTo({top:Math.max(0,c),behavior:"auto"})}},o=()=>{a==null&&(a=requestAnimationFrame(s))};addEventListener("scroll",o,{passive:!0}),addEventListener("resize",o,{passive:!0}),o()}var H=document.querySelector(".gala-action-rail"),$=document.querySelector("[data-action-dock]");if(H&&$){let e=t=>{H.classList.toggle("gala-action-rail--integrated",t[0].isIntersecting)};new IntersectionObserver(e,{rootMargin:"0px 0px 18% 0px",threshold:0}).observe($)}function W(e,t){let n=t?"Unfollow article":"Follow article";e.setAttribute("aria-pressed",String(t)),e.setAttribute("aria-label",n),e.title=n;let a=e.querySelector("[data-follow-label]");a&&(a.textContent=n)}document.addEventListener("click",async e=>{if(e.target instanceof HTMLDialogElement&&e.target.open){let i=e.target.getBoundingClientRect();if(e.clientX<i.left||e.clientX>i.right||e.clientY<i.top||e.clientY>i.bottom){e.target.close();return}}let t=e.target.closest("[data-gala-embed-load]");if(t){let i=t.closest("[data-gala-embed]"),r=t.dataset.galaEmbedSrc,c=t.dataset.galaEmbedLoad;if(!i||!r||!["youtube","codepen"].includes(c))return;let u=document.createElement("iframe");u.src=r,u.title=c==="youtube"?"YouTube video":"CodePen example",u.loading="eager",u.referrerPolicy="strict-origin-when-cross-origin",u.setAttribute("sandbox","allow-forms allow-popups allow-presentation allow-same-origin allow-scripts"),u.setAttribute("allow",c==="youtube"?"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share":"fullscreen"),u.setAttribute("allowfullscreen",""),i.replaceChildren(u);return}let n=e.target.closest("[data-open-dialog]");if(n){let i=document.getElementById(n.dataset.openDialog);i instanceof HTMLDialogElement&&i.showModal();return}let a=e.target.closest("[data-reaction]");if(a){let i=a.closest("[data-engagement-url]");if(!i)return;if(!y)return R({kind:"reaction",region:i,selector:`[data-reaction="${a.dataset.reaction}"]`});let r=a.getAttribute("aria-pressed")!=="true";if(!await B(i,r?"reaction.add":"reaction.remove",{articleId:i.dataset.articleId,reaction:a.dataset.reaction}))return;a.setAttribute("aria-pressed",String(r));return}let s=e.target.closest("[data-follow-article]");if(s){let i=s.closest("[data-engagement-url]");if(!i)return;if(!y)return R({kind:"follow",region:i,selector:"[data-follow-article]"});let r=s.getAttribute("aria-pressed")!=="true";if(!await B(i,r?"follow.add":"follow.remove",{targetType:"articles",targetId:i.dataset.articleId}))return;W(s,r);return}let o=e.target.closest("[data-copy-url]");if(o){let i=o.dataset.copyUrl;if(!window.isSecureContext||!navigator.clipboard?.writeText){v(o);return}try{await navigator.clipboard.writeText(i);let r=o.closest(".gala-share")?.querySelector(".gala-share__status");r&&(r.textContent="Link copied.")}catch{v(o)}return}let d=e.target.closest("[data-native-share]");if(d){if(navigator.share)try{await navigator.share({title:document.title,url:d.dataset.nativeShare})}catch(i){i.name!=="AbortError"&&v(d)}else v(d);return}let f=e.target.closest("[data-copy-code]");if(!f)return;let E=f.closest(".gala-code-block")?.querySelector("code");if(!(!E||!navigator.clipboard?.writeText||!window.isSecureContext))try{await navigator.clipboard.writeText(E.textContent),f.textContent="Copied"}catch{f.textContent="Select code to copy"}});var ee=new Set([...document.querySelectorAll("pre code")].map(e=>e.closest("pre")).filter(Boolean));ee.forEach(e=>{let t=document.createElement("div");t.className="gala-code-block",e.before(t),t.append(e);let n=document.createElement("button");n.type="button",n.dataset.copyCode="",n.textContent="Copy code",n.setAttribute("aria-label","Copy code block"),t.prepend(n)});function te(e){return[["Reactions",Object.values(e.reactions??{}).reduce((n,a)=>n+(Number.isSafeInteger(a)?a:0),0)],["Comments",Number.isSafeInteger(e.comments?.totalCount)?e.comments.totalCount:0],["Views",Number.isSafeInteger(e.views?.count)?e.views.count:0]]}function ne(e){return e<1e3?"<1K":new Intl.NumberFormat("en",{notation:"compact",maximumFractionDigits:1}).format(e)}function ae(e,t){let n=t?.data;if(!n||typeof n!="object")throw new TypeError("Engagement data is invalid");for(let[a,s]of te(n))e.querySelectorAll(`[data-engagement-stat="${a.toLowerCase()}"]`).forEach(o=>{o.textContent=ne(s),o.closest("div")?.setAttribute("title",`${s} ${a.toLowerCase()}`)});e.querySelector("[data-engagement-snapshot]")?.remove(),e.querySelector(".gala-engagement__placeholder")?.remove()}async function x(e,t="",n=!1,a=!1){let s=e.querySelector("[data-engagement-status]"),o=!!e.querySelector("[data-gala-comments]");try{let d=new URL(e.dataset.engagementUrl);t&&d.searchParams.set("commentsCursor",t);let f=await fetch(d,{headers:{Accept:"application/json"},credentials:"omit",cache:a?"no-store":"default"});if(!f.ok)throw new Error(`Engagement returned HTTP ${f.status}`);let E=await f.json();ae(e,E),s&&!o&&(s.textContent=E.errors?.length?"Some engagement data is temporarily unavailable.":"")}catch{s&&!o&&(s.textContent="Engagement is temporarily unavailable.")}}function re(){let e=new URL(window.location.href).searchParams,t={};for(let n of["utm_source","utm_medium","utm_campaign","utm_content","utm_term"]){let a=e.get(n);a&&[...a].length<=128&&(t[n]=a)}return t}function oe(e){let t=new URL(e.dataset.engagementUrl);t.pathname=t.pathname.replace(/\/engagement$/,"/views"),fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({language:document.documentElement.lang||void 0,referrer:document.referrer||void 0,campaign:re()}),credentials:"omit",keepalive:!0}).catch(()=>{})}document.addEventListener("gala-request-sign-in",e=>{y||T||R({kind:e.detail?.kind??"comment"})});var y=null,j=null,_=null,L=new Map,T=null;function R(e){let t=e.field;T={kind:e.kind,region:e.region,selector:e.selector,draft:t?t.value:null,caret:t?t.selectionStart:null};let n=document.querySelector("[data-gala-session-frame]"),a=document.querySelector("[data-engagement-status]");if(!n)return;let s=new URL(n.getAttribute("src"),window.location.href),o=new URL("/v1/widget/session/sign-in",s.origin);o.searchParams.set("siteId",s.searchParams.get("siteId")??""),t&&t.blur(),window.open(o,"gala-sign-in","popup,width=520,height=680")||(T=null,a&&(a.textContent="Allow pop-ups for this site to sign in, then try again."))}function Y(){let e=T;if(!e||!document.hasFocus())return;let t=e.region?.querySelector(e.selector);if(t){if(T=null,e.kind==="comment"){e.draft!=null&&t.value===""&&(t.value=e.draft),t.focus(),e.caret!=null&&typeof t.setSelectionRange=="function"&&t.setSelectionRange(e.caret,e.caret);return}t.click()}}window.addEventListener("focus",()=>{y&&T&&Y()});window.addEventListener("message",e=>{let t=document.querySelector("[data-gala-session-frame]");if(!t||e.data?.type!=="gala-session-established")return;let n=new URL(t.src).origin;e.origin===n&&t.contentWindow?.postMessage({type:"gala-session-recheck"},n)});document.querySelectorAll("[data-engagement-url]").forEach(e=>{x(e),oe(e)});var I=document.querySelector("[data-gala-session-frame]");function V(e){return e==="AUTHENTICATION_REQUIRED"||e==="INVALID_BEARER_TOKEN"||e==="REAUTHENTICATION_REQUIRED"?"Sign in again to continue.":e==="ENGAGEMENT_RATE_LIMITED"?"You are posting too quickly; try again shortly.":e==="CONTACT_RATE_LIMITED"?"You are sending too quickly; try again later.":e==="RESOURCE_NOT_FOUND"?"That item is no longer available.":e==="INVALID_ENGAGEMENT_WRITE"||e==="INVALID_CONTACT_SUBMISSION"||e==="INVALID_REQUEST"?"Check your entry and try again.":e==="ACCESS_DENIED"?"That action is not available for this account.":e==="IDEMPOTENCY_CONFLICT"||e==="ENGAGEMENT_STATE_CONFLICT"?"The item changed; reload and try again.":"The action could not be completed. Try again."}function Q(e,t){if(!I||!y)return Promise.reject(new Error("AUTHENTICATION_REQUIRED"));let n=crypto.randomUUID();return new Promise((a,s)=>{let o=setTimeout(()=>{L.delete(n),s(new Error("REQUEST_TIMEOUT"))},1e4);L.set(n,{resolve:d=>{clearTimeout(o),a(d)},reject:d=>{clearTimeout(o),s(d)}}),I.contentWindow.postMessage({type:"gala-engagement-write",requestId:n,operation:e,payload:t},new URL(I.src).origin)})}document.addEventListener("submit",async e=>{let t=e.target.closest("[data-contact-form] form");if(!t)return;e.preventDefault();let n=t.closest("[data-contact-form]"),a=n.querySelector("[data-contact-status]");if(!y){a.textContent="Sign in with the account button before sending.";return}let s=new FormData(t);try{a.textContent="Sending\u2026",await Q("contact.submit",{siteId:n.dataset.siteId,subject:s.get("subject"),message:s.get("message"),website:s.get("website")||null,phone:s.get("phone")||null}),t.reset(),a.textContent="Message sent."}catch(o){a.textContent=V(o.message)}});async function B(e,t,n){let a=e.querySelector("[data-engagement-status]");try{return a&&(a.textContent="Saving\u2026"),await Q(t,n),a&&(a.textContent="Saved."),await x(e,"",!1,!0),!0}catch(s){return a&&(a.textContent=V(s.message)),!1}}if(I){let e=new URL(I.src).origin;window.addEventListener("message",t=>{if(t.origin!==e||t.source!==I.contentWindow)return;if(t.data?.type==="gala-engagement-result"){let o=L.get(t.data.requestId);if(!o)return;L.delete(t.data.requestId),t.data.ok===!0?o.resolve(t.data.result):o.reject(new Error(t.data.error?.code||"ENGAGEMENT_WRITE_FAILED"));return}if(t.data?.type==="gala-reader-state-result"){if(t.data.requestId!==_||t.data.ok!==!0)return;_=null;let o=new Set(Array.isArray(t.data.state?.reactions)?t.data.state.reactions:[]);document.querySelectorAll("[data-reaction]").forEach(d=>{d.setAttribute("aria-pressed",String(o.has(d.dataset.reaction)))}),document.querySelectorAll("[data-follow-article]").forEach(d=>{let f=t.data.state?.following===!0;W(d,f)});return}if(t.data?.type==="gala-session-height"){let o=Number(t.data.height);Number.isFinite(o)&&o>0&&o<2e3&&(I.height=String(Math.ceil(o)));return}if(t.data?.type!=="gala-session")return;y=t.data.user&&typeof t.data.user.id=="string"?t.data.user:null;let n=document.querySelector("[data-user-control]"),a=y?.displayName;if(n){n.setAttribute("aria-label",a?`Account: ${a}`:"Sign in or view account"),n.title=a?`Account: ${a}`:"Account";let o=n.querySelector("[data-user-label]");o&&(o.textContent=a||"Sign in")}let s=j!==(y?.id??null);if(j=y?.id??null,y){let o=document.querySelector("[data-article-id]");o&&(_=crypto.randomUUID(),I.contentWindow.postMessage({type:"gala-reader-state-request",requestId:_,articleId:o.dataset.articleId},e))}document.querySelectorAll("[data-engagement-url]").forEach(o=>{s&&x(o)}),y&&T&&Y()})}document.querySelectorAll("[data-gala-search]").forEach(e=>{let t=e.querySelector("form"),n=t?.elements.namedItem("q"),a=e.querySelector("[data-search-status]"),s=e.querySelector("[data-search-results]"),o=e.dataset.indexUrl,d,f=c=>String(c??"").normalize("NFKC").toLocaleLowerCase();function E(c,u){s.replaceChildren();for(let m of c){let p=document.createElement("li"),l=document.createElement("article"),h=document.createElement("h2"),w=document.createElement("a");if(w.href=m.url,w.lang=m.language,w.textContent=m.title,h.append(w),l.append(h),m.description){let k=document.createElement("p");k.textContent=m.description,l.append(k)}p.append(l),s.append(p)}a.textContent=`${c.length} result${c.length===1?"":"s"} for \u201C${u}\u201D.`}async function i(c){let u=c.trim();if(u===""){s.replaceChildren(),a.textContent="Enter a search term.";return}a.textContent="Searching\u2026";try{d??=fetch(o,{headers:{Accept:"application/json"}}).then(async l=>{if(!l.ok)throw new Error(`Search index returned HTTP ${l.status}`);let h=await l.json();if(h?.schemaVersion!==1||!Array.isArray(h.entries))throw new TypeError("Search index schema is unsupported");return h.entries});let m=f(u),p=(await d).filter(l=>f([l.title,l.description,l.language,...Array.isArray(l.tags)?l.tags:[],l.body].join(`
2
+ `)).includes(m));E(p,u)}catch{s.replaceChildren(),a.textContent="Search is temporarily unavailable."}}t?.addEventListener("submit",c=>{c.preventDefault();let u=n.value,m=new URL(window.location.href);u.trim()===""?m.searchParams.delete("q"):m.searchParams.set("q",u),window.history.replaceState(null,"",m),i(u)});let r=new URLSearchParams(window.location.search).get("q")??"";n&&(n.value=r),i(r)});var S={value:null},A=document.querySelector("[data-gala-session-frame]"),z=A?new URL(A.src).origin:null,N=new Map;function K(e){return e==="AUTHENTICATION_REQUIRED"||e==="INVALID_BEARER_TOKEN"||e==="REAUTHENTICATION_REQUIRED"?"Sign in again to continue.":e==="ENGAGEMENT_RATE_LIMITED"?"You are posting too quickly; try again shortly.":e==="CONTACT_RATE_LIMITED"?"You are sending too quickly; try again later.":e==="RESOURCE_NOT_FOUND"?"That item is no longer available.":e==="INVALID_ENGAGEMENT_WRITE"||e==="INVALID_CONTACT_SUBMISSION"||e==="INVALID_REQUEST"?"Check your entry and try again.":e==="ACCESS_DENIED"?"That action is not available for this account.":e==="IDEMPOTENCY_CONFLICT"||e==="ENGAGEMENT_STATE_CONFLICT"?"The item changed; reload and try again.":"The action could not be completed. Try again."}function X(e,t){if(!A||!S.value)return Promise.reject(new Error("AUTHENTICATION_REQUIRED"));let n=crypto.randomUUID();return new Promise((a,s)=>{let o=setTimeout(()=>{N.delete(n),s(new Error("REQUEST_TIMEOUT"))},1e4);N.set(n,{resolve:d=>{clearTimeout(o),a(d)},reject:d=>{clearTimeout(o),s(d)}}),A.contentWindow.postMessage({type:"gala-engagement-write",requestId:n,operation:e,payload:t},z)})}function U(e){document.dispatchEvent(new CustomEvent("gala-request-sign-in",{detail:e}))}A&&window.addEventListener("message",e=>{if(e.origin!==z||e.source!==A.contentWindow)return;if(e.data?.type==="gala-engagement-result"){let n=N.get(e.data.requestId);if(!n)return;N.delete(e.data.requestId),e.data.ok===!0?n.resolve(e.data.result):n.reject(new Error(e.data.error?.code||"ENGAGEMENT_WRITE_FAILED"));return}if(e.data?.type!=="gala-session")return;let t=e.data.user;S.value=t&&typeof t.id=="string"?t:null,window.dispatchEvent(new CustomEvent("gala-session-change"))});var se=5,g=(e,t,n)=>{let a=document.createElement(e);return t&&(a.className=t),n!==void 0&&(a.textContent=n),a};function ie(e){let t=new Map;for(let a of e){let s=a.parentCommentId??"";t.has(s)||t.set(s,[]),t.get(s).push(a)}let n=(a,s)=>(t.get(a)??[]).map(o=>({...o,depth:s,replies:n(o.commentId,s+1)}));return n("",0)}function ce(e,t){let n={items:[],cursor:null,total:0,busy:!1,status:"",phase:"loading"},a=/\/v1\/articles\/([^/]+)\/engagement/.exec(t)?.[1]??"",s=e.closest(".gala-conversation")?.querySelector("[data-engagement-status]");async function o(r="",{append:c=!1,fresh:u=!1}={}){let m=new URL(t);r&&m.searchParams.set("commentsCursor",r);let p=await fetch(m,{headers:{Accept:"application/json"},credentials:"omit",cache:u?"no-store":"default"});if(!p.ok)throw new Error(`Comments returned HTTP ${p.status}`);let l=(await p.json())?.data?.comments;if(!l||!Array.isArray(l.items))throw new TypeError("Comment page is invalid");n.items=c?[...n.items,...l.items]:l.items,n.cursor=l.nextCursor??null,Number.isSafeInteger(l.totalCount)&&(n.total=l.totalCount)}async function d(r,c){if(!S.value)return U({kind:"comment"}),!1;n.busy=!0,n.status="",i();try{return await X(r,c),await o("",{fresh:!0}),!0}catch(u){return n.status=K(u.message),!1}finally{n.busy=!1,i()}}function f(r,c,u){let m=g("form","gala-comment__form"),p=g("textarea");p.rows=3,p.value=c,p.placeholder=r,p.setAttribute("aria-label",r);let l=g("button","","Post");return l.type="submit",l.disabled=n.busy,m.append(p,l),m.addEventListener("submit",async h=>{h.preventDefault();let w=p.value.trim();w&&await u(w)&&(p.value="")}),m}function E(r){let c=g("li","gala-comment");c.dataset.commentId=r.commentId,c.dataset.depth=r.depth;let u=g("p","gala-comment__meta");if(u.append(g("strong","",r.author?.displayName??"[deleted]")),r.createdAt){let m=g("time","",new Date(r.createdAt).toLocaleDateString());m.dateTime=r.createdAt,u.append(m)}if(c.append(u,g("p","gala-comment__body",r.deleted?"[deleted]":r.body)),!r.deleted){let m=g("div","gala-comment-actions");if(r.depth<se){let l=g("button","","Reply");l.type="button",l.dataset.replyComment=r.commentId,l.addEventListener("click",()=>{let h=c.querySelector(":scope > .gala-comment__form");if(h){h.remove();return}m.after(f(`Reply to ${r.author?.displayName??"this comment"}`,"",w=>d("comment.create",{articleId:a,parentCommentId:r.commentId,body:w})))}),m.append(l)}if(S.value&&r.author?.userId===S.value.id){let l=g("button","","Edit");l.type="button",l.dataset.editComment=r.commentId,l.addEventListener("click",()=>m.after(f("Edit your comment",r.body??"",w=>d("comment.edit",{articleId:a,commentId:r.commentId,body:w}))));let h=g("button","","Delete");h.type="button",h.dataset.deleteComment=r.commentId,h.addEventListener("click",()=>d("comment.delete",{articleId:a,commentId:r.commentId})),m.append(l,h)}else{let l=g("button","","Report");l.type="button",l.dataset.reportComment=r.commentId,l.addEventListener("click",()=>d("comment.report",{articleId:a,commentId:r.commentId,reason:"OTHER"})),m.append(l)}c.append(m)}if(r.replies.length){let m=g("ol","gala-comment-replies");m.append(...r.replies.map(E)),c.append(m)}return c}function i(){if(s&&(s.textContent=n.phase==="loading"?"Loading comments\u2026":n.phase==="unavailable"?"Comments are temporarily unavailable.":n.status),n.phase!=="ready"){e.replaceChildren();return}let r=g("section","gala-comments-island");if(r.setAttribute("aria-label","Comments"),r.append(g("p","gala-comments__heading",n.total===1?"1 comment":`${n.total} comments`)),S.value)r.append(f("Add a comment","",c=>d("comment.create",{articleId:a,body:c})));else{let c=g("p","gala-comments__prompt"),u=g("button","","Sign in to join the conversation");u.type="button",u.addEventListener("click",()=>U({kind:"comment"})),c.append(u),r.append(c)}if(!n.items.length)r.append(g("p","gala-comments__empty","No comments yet."));else{let c=g("ol","gala-comments");c.append(...ie(n.items).map(E)),r.append(c)}if(n.cursor){let c=g("button","gala-comments__more","Show more comments");c.type="button",c.disabled=n.busy,c.addEventListener("click",async()=>{n.busy=!0,n.status="Loading more comments\u2026",i();try{await o(n.cursor,{append:!0}),n.status=""}catch{n.status="More comments couldn\u2019t be loaded. Try again."}n.busy=!1,i()}),r.append(c)}e.replaceChildren(r)}window.addEventListener("gala-session-change",i),i(),o().then(()=>{n.phase="ready"}).catch(()=>{n.phase="unavailable"}).finally(i)}document.querySelectorAll("[data-gala-comments]").forEach(e=>{let t=e.closest("[data-engagement-url]")?.dataset.engagementUrl;t&&ce(e,t)});})();