PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / assets / js / feature-preview-modal.js
embedpress / assets / js Last commit date
chunks 2 weeks ago vendor 9 months ago admin.build.js 2 weeks ago admin.js 3 months ago analytics-tracker.js 1 month ago analytics.build.js 2 weeks ago blocks.build.js 3 days ago carousel.js 1 year ago custom-player.build.js 2 weeks ago documents-viewer-script.js 3 months ago ep-gr-elementor-control.js 3 days ago ep-pdf-lightbox.js 3 months ago ep-view-count.js 2 weeks ago ep-yt-queue.js 1 month ago feature-notices.js 7 months ago feature-preview-modal.js 3 days ago front.js 3 days ago frontend.build.js 3 months ago gallery-justify.js 2 weeks ago google-reviews.build.js 3 days ago google-reviews.js 3 days ago gutneberg-script.js 2 months ago index.html 7 years ago initCarousel.js 2 years ago initplyr.js 2 months ago instafeed.js 1 month ago instagram-shortcode-generator.js 1 month ago lazy-load.js 6 months ago license.js 3 months ago meetup-timezone.js 3 months ago onboarding.build.js 2 weeks ago pdf-gallery-elementor-editor.js 3 months ago pdf-gallery.js 3 months ago preview.js 9 months ago settings.js 3 months ago sponsored.js 9 months ago
feature-preview-modal.js
558 lines
1 /**
2 * EmbedPress — "What's New" Feature Preview Modal (frontend behaviour).
3 *
4 * Reads the JSON payload printed by FeaturePreviewModal::render(), builds the
5 * split modal DOM, and drives it:
6 * - single feature → no navigation
7 * - 2+ features → carousel with pager dots + Back/Next; "n of N" in the
8 * eyebrow; Next becomes "Done ✓" on the last step; the
9 * changelog link becomes "Skip · See the full changelog".
10 *
11 * Accessibility: dialog role, focus trap, ESC to close, focus returned to the
12 * trigger element on close. Any close path (X / ESC / backdrop / CTA / Done)
13 * persists the dismissal via admin-ajax so the modal won't re-fire for the
14 * same release version.
15 *
16 * Source file: static/js/feature-preview-modal.js
17 * Build output: assets/js/feature-preview-modal.js (mirror until vite build).
18 *
19 * Depends on the localized global `EmbedPressWhatsNew` (ajaxUrl/nonce/action/i18n).
20 */
21 (function () {
22 'use strict';
23
24 var cfg = window.EmbedPressWhatsNew || {};
25 var i18n = cfg.i18n || {};
26
27 function sprintf(tpl, a, b) {
28 return String(tpl).replace('%1$d', a).replace('%2$d', b);
29 }
30
31 // Emit a tracking beacon for any button/link click inside the What's New
32 // modal. Two sinks, both optional and non-blocking:
33 // - a `document` CustomEvent ('embedpress:whatsnew:click') following the
34 // same convention analytics-tracker.js uses ('embedpress:view'), so any
35 // listener (site analytics, integrations) can subscribe;
36 // - a GTM-style `window.dataLayer` push when a dataLayer is present.
37 // `action` names the control (cta / close / next / back / done / dot /
38 // changelog / whatWeCollect); `detail` carries slide + release context.
39 function beacon(action, extra) {
40 var detail = { action: action };
41 if (extra) {
42 Object.keys(extra).forEach(function (k) { detail[k] = extra[k]; });
43 }
44 try {
45 document.dispatchEvent(new CustomEvent('embedpress:whatsnew:click', { detail: detail }));
46 } catch (e) { /* no-op */ }
47 try {
48 window.dataLayer = window.dataLayer || [];
49 window.dataLayer.push({
50 event: 'embedpress_whatsnew_click',
51 embedpress: detail
52 });
53 } catch (e) { /* no-op */ }
54 }
55
56 function el(tag, cls, attrs) {
57 var node = document.createElement(tag);
58 if (cls) { node.className = cls; }
59 if (attrs) {
60 Object.keys(attrs).forEach(function (k) { node.setAttribute(k, attrs[k]); });
61 }
62 return node;
63 }
64
65 // True when `url` points off this site — such links must open in a new tab
66 // so they never navigate the admin away from the current wp-admin screen.
67 // Internal admin URLs (e.g. the Release Notes page) resolve to the same
68 // host and stay in-tab. Empty / "#" / relative anchors are treated internal.
69 function isExternalUrl(url) {
70 if (!url || url === '#') { return false; }
71 try {
72 var target = new URL(url, window.location.href);
73 if (target.protocol !== 'http:' && target.protocol !== 'https:') { return false; }
74 return target.host !== window.location.host;
75 } catch (e) {
76 return false;
77 }
78 }
79
80 // Apply target/rel for an anchor based on whether its href is external.
81 function applyLinkTarget(anchor, external) {
82 if (external) {
83 anchor.setAttribute('target', '_blank');
84 anchor.setAttribute('rel', 'noopener noreferrer');
85 } else {
86 anchor.removeAttribute('target');
87 anchor.removeAttribute('rel');
88 }
89 }
90
91 function init() {
92 var root = document.getElementById('embedpress-whatsnew-root');
93 var dataNode = document.getElementById('embedpress-whatsnew-data');
94 if (!root || !dataNode) { return; }
95
96 var data;
97 try {
98 data = JSON.parse(dataNode.textContent);
99 } catch (e) {
100 return;
101 }
102 if (!data || !data.features || !data.features.length) { return; }
103
104 var modal = new WhatsNewModal(root, data);
105
106 // Developer switch (server-side `embedpress_whatsnew_autoopen` filter,
107 // default true). Auto-open: the modal opens as soon as the user lands on
108 // an EmbedPress page. Click-mode: it stays closed until the user clicks
109 // the flagged EmbedPress menu item — the blinking bubble is the cue.
110 if (cfg.autoOpen === false) {
111 bindMenuOpen(modal);
112 } else {
113 modal.open();
114 }
115 }
116
117 /**
118 * Click-mode: open the modal when the user clicks the EmbedPress admin menu
119 * (the item carrying the blinking "New" bubble). We're already on an
120 * EmbedPress page, so the top-level/current menu link is a same-page click —
121 * intercept it and open the modal instead of just reloading.
122 */
123 function bindMenuOpen(modal) {
124 var slug = cfg.menuSlug || 'embedpress';
125 // The bubble marks the exact menu item; prefer it, then fall back to the
126 // menu anchor by slug.
127 var badge = document.querySelector('#adminmenu .ep-whatsnew-badge');
128 var anchor = badge ? badge.closest('a') : null;
129 if (!anchor) {
130 anchor = document.querySelector('#adminmenu a[href*="page=' + slug + '"]');
131 }
132 if (!anchor) { return; }
133
134 anchor.addEventListener('click', function (e) {
135 // Only hijack a same-page click (already on an EmbedPress page);
136 // let normal navigation proceed otherwise.
137 e.preventDefault();
138 modal.open();
139 });
140 }
141
142 function WhatsNewModal(root, data) {
143 this.root = root;
144 this.data = data;
145 this.features = data.features;
146 this.multi = this.features.length > 1;
147 this.index = 0;
148 this.dismissed = false;
149 this.lastFocused = null;
150 this.build();
151 }
152
153 WhatsNewModal.prototype.build = function () {
154 var self = this;
155 this.root.removeAttribute('aria-hidden');
156 this.root.className = 'epwn' + (this.multi ? ' epwn--multi' : '');
157 this.root.setAttribute('role', 'dialog');
158 this.root.setAttribute('aria-modal', 'true');
159 this.root.setAttribute('aria-labelledby', 'epwn-title');
160
161 // --- modal shell ---
162 var modal = el('div', 'epwn__modal');
163
164 var close = el('button', 'epwn__close', {
165 type: 'button',
166 'aria-label': i18n.close || 'Close'
167 });
168 close.innerHTML = '✕';
169 close.addEventListener('click', function () { self.beaconClick('close'); self.close(); });
170 modal.appendChild(close);
171
172 // --- left / preview ---
173 var left = el('div', 'epwn__left');
174 this.badge = el('span', 'epwn__badge');
175 left.appendChild(this.badge);
176 this.demo = el('div', 'epwn__demo');
177 var bar = el('div', 'epwn__bar');
178 bar.innerHTML = '<i></i><i></i><i></i>';
179 this.stage = el('div', 'epwn__stage');
180 this.demo.appendChild(bar);
181 this.demo.appendChild(this.stage);
182 left.appendChild(this.demo);
183 modal.appendChild(left);
184
185 // --- right / copy ---
186 var right = el('div', 'epwn__right');
187 this.right = right;
188
189 // brand header: EmbedPress logo (version lives in the eyebrow)
190 var header = el('div', 'epwn__header');
191 if (cfg.logoUrl) {
192 var logo = el('img', 'epwn__logo', { src: cfg.logoUrl, alt: 'EmbedPress' });
193 header.appendChild(logo);
194 }
195 right.appendChild(header);
196
197 this.eyebrow = el('span', 'epwn__eyebrow');
198 this.title = el('h2', 'epwn__title', { id: 'epwn-title' });
199 this.desc = el('p', 'epwn__desc');
200 this.cta = el('a', 'epwn__cta', { href: '#' });
201 this.cta.addEventListener('click', function (e) {
202 self.beaconClick('cta', { label: self.cta.textContent, url: self.cta.getAttribute('href') });
203 // CTA counts as engagement → dismiss, then let the link proceed.
204 self.persistDismiss();
205 if (!self.cta.getAttribute('href') || self.cta.getAttribute('href') === '#') {
206 e.preventDefault();
207 self.close();
208 }
209 });
210
211 right.appendChild(this.eyebrow);
212 right.appendChild(this.title);
213 right.appendChild(this.desc);
214 right.appendChild(el('div', 'epwn__grow'));
215 right.appendChild(this.cta);
216
217 // nav (always built; shown only when multi via CSS)
218 var nav = el('div', 'epwn__nav');
219 this.backBtn = el('button', 'epwn__back', { type: 'button' });
220 this.backBtn.textContent = i18n.back || '← Back';
221 this.dots = el('div', 'epwn__dots');
222 this.nextBtn = el('button', 'epwn__next', { type: 'button' });
223 this.backBtn.addEventListener('click', function () { self.beaconClick('back'); self.prev(); });
224 this.nextBtn.addEventListener('click', function () {
225 self.beaconClick(self.index < self.features.length - 1 ? 'next' : 'done');
226 self.next();
227 });
228 nav.appendChild(this.backBtn);
229 nav.appendChild(this.dots);
230 nav.appendChild(this.nextBtn);
231 right.appendChild(nav);
232
233 // Bottom link. Text + href are set PER SLIDE in render(): the changelog
234 // link on every slide, except the last slide becomes "What we collect"
235 // → privacy policy when tracking isn't opted-in.
236 // - changelog link → opens in a new tab AND dismisses the modal.
237 // - "What we collect" → opens the policy in a new tab ONLY; the popup
238 // stays open (it's not a dismissal, just a reference link).
239 var chg = el('div', 'epwn__chglink');
240 this.chgLink = el('a', null, { href: this.data.changelogUrl || '#', target: '_blank', rel: 'noopener noreferrer' });
241 this.chgLink.addEventListener('click', function (e) {
242 e.preventDefault();
243 self.beaconClick(self.chgLinkIsWhatWeCollect ? 'whatWeCollect' : 'changelog', {
244 url: self.chgLink.getAttribute('href')
245 });
246 var url = self.chgLink.getAttribute('href');
247 if (url && url !== '#') {
248 window.open(url, '_blank', 'noopener,noreferrer');
249 }
250 // "What we collect" is a reference link — do NOT close/dismiss.
251 if (!self.chgLinkIsWhatWeCollect) {
252 self.close();
253 }
254 });
255 chg.appendChild(this.chgLink);
256 right.appendChild(chg);
257
258 modal.appendChild(right);
259 this.root.appendChild(modal);
260
261 // backdrop click closes
262 this.root.addEventListener('mousedown', function (e) {
263 if (e.target === self.root) { self.close(); }
264 });
265
266 // keyboard: ESC + focus trap
267 this.keyHandler = function (e) { self.onKeydown(e); };
268 document.addEventListener('keydown', this.keyHandler);
269
270 this.render();
271 };
272
273 WhatsNewModal.prototype.render = function () {
274 var f = this.features[this.index];
275 var multi = this.multi;
276
277 // media
278 this.stage.innerHTML = '';
279 var m = f.media || {};
280 this.badge.textContent = m.badge || '';
281 this.badge.style.display = m.badge ? '' : 'none';
282
283 // File media (image/gif/video) has a natural aspect ratio and should
284 // size to its content — the demo panel then centers in the column
285 // rather than stretching to full height (which gaps a landscape clip).
286 // html demos (e.g. Google Reviews) are built to fill, so they keep it.
287 this.demo.classList.toggle('epwn__demo--flat', m.type !== 'html');
288
289 if (m.type === 'html' && m.html) {
290 this.stage.innerHTML = m.html;
291 } else if (m.type === 'video' && m.src) {
292 // A CDN-hosted clip can lag on first load; show the shimmer until
293 // the first frame is ready, then reveal (same affordance as images).
294 var vstage = this.stage;
295 vstage.classList.add('epwn__stage--loading');
296 var v = el('video', null, { src: m.src, autoplay: '', muted: '', loop: '', playsinline: '', preload: 'auto' });
297 v.muted = true;
298 if (m.poster) { v.setAttribute('poster', m.poster); }
299 var vreveal = function () { vstage.classList.remove('epwn__stage--loading'); };
300 v.addEventListener('loadeddata', vreveal);
301 v.addEventListener('error', vreveal);
302 this.stage.appendChild(v);
303 } else if (m.src) {
304 // Large images/GIFs may still be downloading when the modal opens
305 // (they're preloaded on page load, but a cold cache can lag). Show a
306 // shimmer on the stage until the image decodes, then reveal it.
307 var stage = this.stage;
308 stage.classList.add('epwn__stage--loading');
309 var img = el('img', null, { src: m.src, alt: f.title || '' });
310 var reveal = function () { stage.classList.remove('epwn__stage--loading'); };
311 if (img.complete) { reveal(); }
312 else {
313 img.addEventListener('load', reveal);
314 img.addEventListener('error', reveal);
315 }
316 stage.appendChild(img);
317 }
318
319 // copy
320 this.eyebrow.textContent = multi
321 ? f.eyebrow + ' · ' + sprintf(i18n.counter || '%1$d of %2$d', this.index + 1, this.features.length)
322 : f.eyebrow;
323 this.title.textContent = f.title;
324 this.desc.innerHTML = f.desc; // server-side wp_kses_post'd
325
326 // cta
327 if (f.cta && f.cta.label) {
328 this.cta.textContent = f.cta.label;
329 this.cta.style.display = '';
330 this.cta.setAttribute('href', f.cta.url || '#');
331 // Honour the explicit `external` flag, but also open in a new tab
332 // for any off-site URL so a doc/changelog CTA never navigates the
333 // admin away from the current screen even if the flag was omitted.
334 applyLinkTarget(this.cta, f.cta.external || isExternalUrl(f.cta.url));
335 } else {
336 this.cta.style.display = 'none';
337 }
338
339 // bottom-link copy + href, per slide. On the FIRST slide, and only when
340 // whatWeCollectUrl is set (tracking not yet opted-in), the link becomes
341 // "What we collect" → privacy policy — the disclosure shown up front,
342 // before the user clicks Next (which is what grants consent). Every
343 // other slide keeps the changelog / "Skip" link.
344 var isFirst = this.index === 0;
345 if (this.data.whatWeCollectUrl && isFirst) {
346 this.chgLink.textContent = i18n.whatWeCollect || 'What we collect';
347 this.chgLink.setAttribute('href', this.data.whatWeCollectUrl);
348 // Mark this as the reference link so its click won't dismiss.
349 this.chgLinkIsWhatWeCollect = true;
350 } else {
351 this.chgLink.textContent = multi ? (i18n.skip || 'Skip') : (i18n.changelog || 'See the full changelog');
352 this.chgLink.setAttribute('href', this.data.changelogUrl || '#');
353 this.chgLinkIsWhatWeCollect = false;
354 }
355
356 // fade the copy column
357 this.right.classList.remove('epwn__fade');
358 void this.right.offsetWidth;
359 this.right.classList.add('epwn__fade');
360
361 // nav state
362 if (multi) {
363 this.dots.innerHTML = '';
364 for (var k = 0; k < this.features.length; k++) {
365 var dot = el('button', 'epwn__dot' + (k === this.index ? ' is-on' : ''), {
366 type: 'button',
367 'aria-label': sprintf(i18n.counter || '%1$d of %2$d', k + 1, this.features.length)
368 });
369 (function (idx, modalRef) {
370 dot.addEventListener('click', function () {
371 modalRef.beaconClick('dot', { to: idx + 1 });
372 modalRef.goTo(idx);
373 });
374 })(k, this);
375 this.dots.appendChild(dot);
376 }
377 this.backBtn.style.visibility = this.index > 0 ? 'visible' : 'hidden';
378 this.nextBtn.textContent = this.index < this.features.length - 1
379 ? (i18n.next || 'Next →')
380 : (i18n.done || 'Done ✓');
381 }
382 };
383
384 // Emit a click beacon carrying this modal's context (release version +
385 // current slide) merged with per-control extras. Every interactive control
386 // in the modal routes through here so a listener sees a uniform payload.
387 WhatsNewModal.prototype.beaconClick = function (action, extra) {
388 var f = this.features[this.index] || {};
389 var base = {
390 version: this.data.version || '',
391 release: this.data.id || '',
392 slide: this.index + 1,
393 slides: this.features.length,
394 title: f.title || ''
395 };
396 if (extra) {
397 Object.keys(extra).forEach(function (k) { base[k] = extra[k]; });
398 }
399 beacon(action, base);
400 // Consent is Next/Done only — advancing past the "What we collect"
401 // disclosure on slide 1 is the affirmative action. Dismissing (close /
402 // X / ESC / backdrop / Back / dots / reference link) never enables
403 // tracking. Fired once; server-side is idempotent and only wired when
404 // tracking isn't already on.
405 if (action === 'next' || action === 'done') {
406 this.persistConsent();
407 }
408 };
409
410 // Fire-and-forget the consent opt-in. Guarded so it posts at most once per
411 // modal instance, and only when the server flagged consent as needed
412 // (tracking not already enabled).
413 WhatsNewModal.prototype.persistConsent = function () {
414 if (this.consented) { return; }
415 if (!cfg.consentNeeded || !cfg.ajaxUrl || !cfg.consentAction) { return; }
416 this.consented = true;
417 var body = new URLSearchParams();
418 body.set('action', cfg.consentAction);
419 body.set('nonce', cfg.nonce || '');
420 fetch(cfg.ajaxUrl, {
421 method: 'POST',
422 credentials: 'same-origin',
423 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
424 body: body.toString(),
425 keepalive: true
426 }).catch(function () {});
427 };
428
429 WhatsNewModal.prototype.goTo = function (i) {
430 if (i < 0 || i > this.features.length - 1 || i === this.index) { return; }
431 this.index = i;
432 this.render();
433 };
434
435 WhatsNewModal.prototype.next = function () {
436 if (this.index < this.features.length - 1) {
437 this.index++;
438 this.render();
439 } else {
440 // "Done ✓" on the last step closes + dismisses.
441 this.close();
442 }
443 };
444
445 WhatsNewModal.prototype.prev = function () {
446 if (this.index > 0) {
447 this.index--;
448 this.render();
449 }
450 };
451
452 WhatsNewModal.prototype.focusables = function () {
453 return this.root.querySelectorAll(
454 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'
455 );
456 };
457
458 WhatsNewModal.prototype.onKeydown = function (e) {
459 if (e.key === 'Escape') {
460 e.preventDefault();
461 this.close();
462 return;
463 }
464 if (e.key === 'Tab') {
465 var items = Array.prototype.filter.call(this.focusables(), function (n) {
466 return n.offsetParent !== null && n.style.display !== 'none' && n.style.visibility !== 'hidden';
467 });
468 if (!items.length) { return; }
469 var first = items[0];
470 var last = items[items.length - 1];
471 if (e.shiftKey && document.activeElement === first) {
472 e.preventDefault();
473 last.focus();
474 } else if (!e.shiftKey && document.activeElement === last) {
475 e.preventDefault();
476 first.focus();
477 }
478 }
479 };
480
481 WhatsNewModal.prototype.open = function () {
482 var self = this;
483 this.lastFocused = document.activeElement;
484 // Mark "opened" (NOT "seen"): clears the menu "New" indicator now that
485 // the modal has been opened at least once — but does NOT dismiss the
486 // modal. The modal keeps re-showing until a real dismiss/Done stamps
487 // "seen" via persistDismiss(). These are two independent markers.
488 this.persistOpened();
489 var badge = document.querySelector('#adminmenu .ep-whatsnew-badge');
490 if (badge && badge.parentNode) { badge.parentNode.removeChild(badge); }
491 // next frame so the transition runs
492 requestAnimationFrame(function () {
493 self.root.classList.add('is-open');
494 var first = self.focusables()[0];
495 if (first) { first.focus(); }
496 });
497 };
498
499 // Fire-and-forget the "opened" marker. Guarded so it only posts once per
500 // modal instance.
501 WhatsNewModal.prototype.persistOpened = function () {
502 if (this.opened) { return; }
503 this.opened = true;
504 if (!cfg.ajaxUrl || !cfg.openedAction) { return; }
505 var body = new URLSearchParams();
506 body.set('action', cfg.openedAction);
507 body.set('nonce', cfg.nonce || '');
508 body.set('version', this.data.version || '');
509 fetch(cfg.ajaxUrl, {
510 method: 'POST',
511 credentials: 'same-origin',
512 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
513 body: body.toString(),
514 keepalive: true
515 }).catch(function () {});
516 };
517
518 WhatsNewModal.prototype.persistDismiss = function () {
519 if (this.dismissed) { return; }
520 this.dismissed = true;
521 if (!cfg.ajaxUrl || !cfg.action) { return; }
522 var body = new URLSearchParams();
523 body.set('action', cfg.action);
524 body.set('nonce', cfg.nonce || '');
525 body.set('version', this.data.version || '');
526 // Fire-and-forget; keepalive so it survives navigation on CTA click.
527 fetch(cfg.ajaxUrl, {
528 method: 'POST',
529 credentials: 'same-origin',
530 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
531 body: body.toString(),
532 keepalive: true
533 }).catch(function () {});
534 };
535
536 WhatsNewModal.prototype.close = function () {
537 var self = this;
538 this.persistDismiss();
539 // (The menu badge was already removed on open() — clearing it there is
540 // what decouples "seen the badge" from "dismissed the modal".)
541 this.root.classList.remove('is-open');
542 document.removeEventListener('keydown', this.keyHandler);
543 setTimeout(function () {
544 self.root.setAttribute('aria-hidden', 'true');
545 self.root.innerHTML = '';
546 }, 240);
547 if (this.lastFocused && this.lastFocused.focus) {
548 this.lastFocused.focus();
549 }
550 };
551
552 if (document.readyState === 'loading') {
553 document.addEventListener('DOMContentLoaded', init);
554 } else {
555 init();
556 }
557 })();
558