PageSourceSearch

https://www.arsenalinsider.com/wp-content/plugins/talkingpoints-wp…/js/featured-comments.js?ver=1787750311

js arsenalinsider.com collected 2026-10-02 11:04:57 UTC 11,833 bytes, 447 lines download raw bytes

1/* global DOMPurify */
2(function () {
3  /**
4   * @typedef {Object} TalkingPointsApi
5   * @property {string=} publicUrl
6   * @property {string=} fallbackAvatarUrl
7   * @property {string=} likeIconUrl
8   */
9
10  /** @type {TalkingPointsApi} */
11  const api = window.TalkingPointsApi || {};
12  const baseUrl = api.publicUrl;
13  if (!baseUrl) return;
14
15  function sanitizeUserHtml(value) {
16    if (!value) {
17      return false;
18    }
19
20    if (typeof DOMPurify === 'undefined') {
21      return false;
22    }
23
24    if (!DOMPurify.isSupported) {
25      return false;
26    }
27
28    return DOMPurify.sanitize(value);
29  }
30
31  function buildFeaturedItem(author, body, likes) {
32    const itemContent = document.createElement('div');
33    itemContent.className = 'tp-featured-item';
34
35    const header = document.createElement('div');
36    header.className = 'tp-featured-header';
37
38    const authorWrap = document.createElement('div');
39    authorWrap.className = 'tp-featured-author';
40
41    let userAvatar = window.TalkingPointsApi?.publicAvatarUrl ?? '';
42    userAvatar = userAvatar.replace('{username}', author);
43
44    const avatar = document.createElement('img');
45    avatar.className = 'tp-featured-avatar';
46    avatar.setAttribute('alt', '');
47    avatar.setAttribute('loading', 'lazy');
48    avatar.setAttribute('decoding', 'async');
49    avatar.width = 30;
50    avatar.height = 30;
51    avatar.src = userAvatar || api.fallbackAvatarUrl || '';
52
53    if (!userAvatar) {
54      avatar.classList.add('fallback');
55    }
56
57    const name = document.createElement('span');
58    name.className = 'tp-featured-name';
59    name.textContent = author || 'Anonymous';
60
61    authorWrap.appendChild(avatar);
62    authorWrap.appendChild(name);
63
64    const featuredTag = document.createElement('div');
65    featuredTag.className = 'tp-featured-tag';
66    featuredTag.textContent = 'Featured comment';
67
68    const bodyElement = document.createElement('p');
69    bodyElement.className = 'tp-featured-body';
70
71    const sanitized = sanitizeUserHtml(body);
72
73    if (sanitized) {
74      bodyElement.innerHTML = sanitized;
75    } else {
76      bodyElement.textContent = body;
77    }
78
79    const actions = document.createElement('div');
80    actions.className = 'tp-featured-actions';
81
82    if (likes > 0) {
83      const like = document.createElement('span');
84      like.className = 'tp-featured-like';
85
86      const icon = document.createElement('img');
87      icon.className = 'tp-featured-like-icon';
88      icon.setAttribute('alt', '');
89      icon.setAttribute('aria-hidden', 'true');
90      icon.width = 16;
91      icon.height = 16;
92      icon.src = api.likeIconUrl || '';
93
94      const count = document.createElement('span');
95      count.className = 'tp-featured-like-count';
96      count.textContent = String(likes);
97
98      like.appendChild(icon);
99      like.appendChild(count);
100      actions.appendChild(like);
101    }
102
103    header.appendChild(authorWrap);
104    header.appendChild(featuredTag);
105    itemContent.appendChild(header);
106    itemContent.appendChild(bodyElement);
107
108    if (actions.childNodes.length) {
109      itemContent.appendChild(actions);
110    }
111
112    return itemContent;
113  }
114
115  function getTrackGapPx(track) {
116    const computedStyle = window.getComputedStyle(track);
117    return parseFloat(computedStyle.columnGap || computedStyle.gap || 0) || 0;
118  }
119
120  function updateDotsActiveState(dots, activeIndex) {
121    for (let dotIndex = 0; dotIndex < dots.length; dotIndex++) {
122      const dot = dots[dotIndex];
123      if (!dot) {
124        continue;
125      }
126
127      if (dotIndex === activeIndex) {
128        dot.setAttribute('aria-current', 'true');
129        dot.classList.add('is-active');
130      } else {
131        dot.removeAttribute('aria-current');
132        dot.classList.remove('is-active');
133      }
134    }
135  }
136
137  function setTrackOffset(track, slides, activeIndex) {
138    const firstSlide = slides[0];
139    const slideWidth = firstSlide ? firstSlide.offsetWidth : 0;
140    const gapPx = getTrackGapPx(track);
141    track.style.marginLeft = String(-(activeIndex * (slideWidth + gapPx))) + 'px';
142  }
143
144  function setSlideWidths(slides, width) {
145    for (let slideIndex = 0; slideIndex < slides.length; slideIndex++) {
146      slides[slideIndex].style.width = String(width) + 'px';
147    }
148  }
149
150  function updateCarouselHeight(carousel, slides) {
151    let maxHeight = 0;
152
153    for (let slideIndex = 0; slideIndex < slides.length; slideIndex++) {
154      const height = slides[slideIndex].offsetHeight;
155      if (height > maxHeight) {
156        maxHeight = height;
157      }
158    }
159
160    if (maxHeight) {
161      carousel.style.height = String(maxHeight) + 'px';
162    }
163  }
164
165  function createCarouselDom(items) {
166    const carousel = document.createElement('div');
167    carousel.className = 'tp-carousel';
168    carousel.setAttribute('role', 'region');
169    carousel.setAttribute('aria-label', 'Featured comments');
170    carousel.setAttribute('aria-live', 'polite');
171
172    const track = document.createElement('div');
173    track.className = 'tp-track';
174
175    const slides = [];
176    const dots = [];
177
178    const dotsWrap = items.length > 1 ? document.createElement('div') : null;
179    if (dotsWrap) {
180      dotsWrap.className = 'tp-dots';
181      carousel.classList.add('has-dots');
182    }
183
184    for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
185      const item = items[itemIndex];
186
187      const slide = document.createElement('div');
188      slide.className = 'tp-slide';
189      slide.appendChild(item);
190
191      slides.push(slide);
192      track.appendChild(slide);
193
194      if (!dotsWrap) {
195        continue;
196      }
197
198      const dot = document.createElement('button');
199      dot.type = 'button';
200      dot.className = 'tp-dot';
201      dot.setAttribute('aria-label', 'Go to comment ' + String(itemIndex + 1));
202
203      dots.push(dot);
204      dotsWrap.appendChild(dot);
205    }
206
207    carousel.appendChild(track);
208
209    if (dotsWrap) {
210      carousel.appendChild(dotsWrap);
211    }
212
213    return {
214      carousel: carousel,
215      track: track,
216      slides: slides,
217      dots: dots,
218      dotsWrap: dotsWrap
219    };
220  }
221
222  function attachCarouselBehavior(dom) {
223    const slides = dom.slides;
224    const dots = dom.dots;
225
226    let activeIndex = 0;
227    let timerId = null;
228    let layoutScheduled = false;
229
230    function goTo(nextIndex) {
231      if (!slides.length) {
232        return;
233      }
234
235      activeIndex = (nextIndex + slides.length) % slides.length;
236
237      setTrackOffset(dom.track, slides, activeIndex);
238      updateDotsActiveState(dots, activeIndex);
239    }
240
241    function stop() {
242      if (!timerId) {
243        return;
244      }
245
246      clearInterval(timerId);
247      timerId = null;
248    }
249
250    function start() {
251      stop();
252      timerId = window.setInterval(function () {
253        goTo(activeIndex + 1);
254      }, 5000);
255    }
256
257    function layout() {
258      layoutScheduled = false;
259
260      const width = dom.carousel.getBoundingClientRect().width;
261      setSlideWidths(slides, width);
262      updateCarouselHeight(dom.carousel, slides);
263      goTo(activeIndex);
264    }
265
266    function scheduleLayout() {
267      if (layoutScheduled) {
268        return;
269      }
270
271      layoutScheduled = true;
272
273      requestAnimationFrame(function () {
274        requestAnimationFrame(layout);
275      });
276    }
277
278    function onMouseEnter() {
279      stop();
280    }
281
282    function onMouseLeave() {
283      if (slides.length > 1) {
284        start();
285      }
286    }
287
288    dom.carousel.addEventListener('mouseenter', onMouseEnter);
289    dom.carousel.addEventListener('mouseleave', onMouseLeave);
290
291    for (let dotIndex = 0; dotIndex < dots.length; dotIndex++) {
292      const dot = dots[dotIndex];
293      dot.addEventListener('click', function (event) {
294        event.preventDefault();
295        event.stopPropagation();
296
297        goTo(dotIndex);
298        start();
299      });
300    }
301
302    if (window.ResizeObserver) {
303      const resizeObserver = new ResizeObserver(function () {
304        scheduleLayout();
305      });
306
307      resizeObserver.observe(dom.carousel);
308    } else {
309      window.addEventListener('resize', function () {
310        scheduleLayout();
311      });
312    }
313
314    scheduleLayout();
315    goTo(0);
316
317    if (slides.length > 1) {
318      start();
319    }
320
321    return {
322      element: dom.carousel,
323      goTo: goTo,
324      start: start,
325      stop: stop
326    };
327  }
328
329  function createCarouselController(items) {
330    const dom = createCarouselDom(items);
331    return attachCarouselBehavior(dom);
332  }
333
334  function requestFeaturedReplies(commentId) {
335    const fullUrl = new URL(`comment/${encodeURIComponent(commentId)}/replies/featured`, baseUrl);
336    return fetch(fullUrl, {
337      method: 'GET',
338      headers: { 'content-type': 'application/json' }
339    }).then(function (response) {
340      if (!response.ok) {
341        throw new Error('bad');
342      }
343
344      return response.json();
345    });
346  }
347
348  function renderFeaturedCarousel(slotElement, items) {
349    if (!items.length) {
350      return;
351    }
352
353    const carouselController = createCarouselController(items);
354    slotElement.appendChild(carouselController.element);
355    slotElement.removeAttribute('hidden');
356  }
357
358  function buildFeaturedItemsFromEdges(chosenEdges) {
359    const items = [];
360
361    for (let index = 0; index < chosenEdges.length; index++) {
362      const node = chosenEdges[index] && chosenEdges[index].node;
363      if (!node) {
364        continue;
365      }
366
367      const author = node.author && node.author.username;
368      const body = node.body || '';
369      const likes = node.actionCounts && node.actionCounts.reaction ? node.actionCounts.reaction.total : 0;
370
371      if (body.trim() !== '') {
372        items.push(buildFeaturedItem(author, body, likes));
373      }
374    }
375
376    return items;
377  }
378
379  function getTalkingPointFeaturedContext(talkingPointElement) {
380    const commentId = talkingPointElement.getAttribute('data-comment-id') || '';
381    const slotElement = talkingPointElement.querySelector('[data-featured-slot]');
382
383    if (!commentId || !slotElement) {
384      return null;
385    }
386
387    return { commentId: commentId, slotElement: slotElement };
388  }
389
390  function loadFeaturedReplies(commentId, slotElement) {
391    requestFeaturedReplies(commentId)
392      .then(function (payload) {
393        const connection = payload && payload.data && payload.data.comment && payload.data.comment.replies;
394        const edges = connection && Array.isArray(connection.edges) ? connection.edges : [];
395
396        if (!edges.length) {
397          return;
398        }
399
400        const items = buildFeaturedItemsFromEdges(edges);
401        renderFeaturedCarousel(slotElement, items);
402      })
403      .catch(function () {
404        // Silent fail
405      });
406  }
407
408  function initFeaturedComments(root) {
409    const scope = root || document;
410    const talkingPointElements = scope.querySelectorAll('.grv-talking-point[data-comment-id]');
411
412    talkingPointElements.forEach(function (talkingPointElement) {
413      if (talkingPointElement.getAttribute('data-featured-disabled') === '1') {
414        return;
415      }
416
417      if (talkingPointElement.getAttribute('data-featured-initialized') === '1') {
418        return;
419      }
420
421      // No replies means no featured reply is possible; skip the fetch.
422      const knownCount = talkingPointElement.getAttribute('data-reply-count-known');
423      if (knownCount !== null && Number(knownCount) <= 0) {
424        talkingPointElement.setAttribute('data-featured-initialized', '1');
425        return;
426      }
427
428      const context = getTalkingPointFeaturedContext(talkingPointElement);
429      if (!context) {
430        return;
431      }
432
433      talkingPointElement.setAttribute('data-featured-initialized', '1');
434
435      loadFeaturedReplies(context.commentId, context.slotElement);
436    });
437  }
438
439  initFeaturedComments();
440
441  // Dispatched by the endless-scroll feature on article pages when a new article loads.
442  document.addEventListener('infscrollpage', function () {
443    initFeaturedComments();
444  });
445
446  window.TPWPFeaturedComments = { init: initFeaturedComments };
447})();

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.