PageSourceSearch

https://openssf.org/wp-content/themes/salient-child/vc-addons/js/members-api-carousel.js?ver=1790129800

js openssf.org collected 2026-09-24 08:26:01 UTC 11,971 bytes, 343 lines download raw bytes

1/**
2 * MEMBERS API CAROUSEL - LINUX FOUNDATION
3 *
4 * FETCHES MEMBER LOGOS FROM THE LF PROJECT API AND INITIALIZES A SLICK
5 * CAROUSEL IN ONE OF THREE MODES:
6 *
7 *   scroll — ALWAYS SCROLL. LOGOS ARE PADDED TO MINIMUM AND AUTOPLAY
8 *            CONTINUOUSLY REGARDLESS OF HOW MANY LOGOS THE API RETURNS.
9 *
10 *   static — ALWAYS STATIC. ALL LOGOS ARE SHOWN AT ONCE WITHOUT MOTION.
11 *            SLICK STILL INITIALIZES SO ALL CARD STYLES APPLY IDENTICALLY.
12 *
13 *   auto   — DETECT AT RUNTIME. MEASURES THE CONTAINER WIDTH AND TOTAL
14 *            LOGO WIDTH; SCROLLS IF LOGOS OVERFLOW, OTHERWISE STATIC.
15 *            THIS IS THE DEFAULT WHEN NO data-display-mode IS SET.
16 *
17 * DISPLAY MODE IS READ FROM data-display-mode ON THE SLIDER ELEMENT,
18 * SET BY THE WPBAKERY BACKEND DROPDOWN IN members-api-carousel.php.
19 *
20 * SUPPORTS MULTIPLE CAROUSEL INSTANCES ON THE SAME PAGE — EACH READS
21 * ITS CONFIG FROM ITS OWN ELEMENT ATTRIBUTES SO INSTANCES NEVER COLLIDE.
22 *
23 * DEPENDS ON: jQuery, Slick 1.9.0
24 * DATA PASSED VIA: data-api-url ATTRIBUTE ON .slider.right.members
25 *
26 * LEGACY FALLBACK: IF data-api-url IS ABSENT, FALLS BACK TO THE
27 * membersCarouselData.apiUrl GLOBAL (OLD wp_localize_script PATH).
28 * IF THAT IS ALSO ABSENT, FALLS BACK TO THE members_url BARE GLOBAL
29 * (ORIGINAL INLINE SCRIPT PATH — SUPPORTS UN-UPDATED SITES).
30 */
31
32jQuery(document).ready(function ($) {
33
34   'use strict';
35
36   // ---------------------------------------------------------------------------
37   // CONSTANTS
38   // ---------------------------------------------------------------------------
39
40   /**
41    * ESTIMATED WIDTH IN PIXELS OF A SINGLE SLIDE SLOT INCLUDING PADDING
42    * AND MARGIN AS DEFINED IN THE EXISTING .slick-slide CSS RULES.
43    * LOGO (75px) + PADDING LEFT+RIGHT (30px) + MARGIN-RIGHT (15px) = 120px.
44    * ADJUST IF THE CSS VALUES EVER CHANGE.
45    */
46   var SLOT_WIDTH_PX = 120;
47
48   /**
49    * BASE SLICK CONFIGURATION SHARED BY BOTH SCROLL AND STATIC MODES.
50    * MOTION-SPECIFIC KEYS ARE OVERRIDDEN PER MODE IN renderCarousel().
51    * variableWidth IS INTENTIONAL — MEMBER LOGOS VARY IN ASPECT RATIO.
52    */
53   var SLICK_CONFIG = {
54      infinite: true,
55      slidesToShow: 6,
56      slidesToScroll: 1,
57      swipeToSlide: false,
58      draggable: false,
59      arrows: false,
60      autoplay: true,
61      autoplaySpeed: 0,
62      cssEase: 'linear',
63      centerMode: true,
64      variableWidth: true,
65      lazyLoad: 'ondemand',
66      pauseOnHover: false,
67      pauseOnFocus: false,
68      speed: 4500,
69      responsive: [
70         {
71            breakpoint: 1200,
72            settings: { slidesToShow: 5 }
73         },
74         {
75            breakpoint: 900,
76            settings: { slidesToShow: 3 }
77         },
78         {
79            breakpoint: 600,
80            settings: { slidesToShow: 2 }
81         }
82      ]
83   };
84
85   // ---------------------------------------------------------------------------
86   // DATA FETCHING
87   // ---------------------------------------------------------------------------
88
89   /**
90    * FETCH MEMBER DATA FROM THE API.
91    * HTML ENTITY DECODE HANDLES & FROM WPBAKERY ATTRIBUTE STORAGE.
92    *
93    * @param  {string}          apiUrl  THE API ENDPOINT URL.
94    * @return {Promise<Array>}
95    */
96   function fetchMembers(apiUrl) {
97      var url = apiUrl.replace(/&amp;/g, '&');
98
99      return fetch(url).then(function (response) {
100         if (!response.ok) {
101            throw new Error('HTTP ' + response.status);
102         }
103         return response.json();
104      });
105   }
106
107   // ---------------------------------------------------------------------------
108   // SLIDE BUILDING
109   // ---------------------------------------------------------------------------
110
111   /**
112    * FILTER RAW API DATA DOWN TO ENTRIES THAT HAVE A USABLE LOGO URL.
113    *
114    * @param  {Array}  data  RAW ARRAY FROM THE API.
115    * @return {Array}        ARRAY OF { logo, website } OBJECTS.
116    */
117   function extractValidMembers(data) {
118      return data.reduce(function (acc, member) {
119         var logo = member.Logo;
120
121         // SKIP ENTRIES WITH NO LOGO OR A LITERAL "undefined" STRING
122         if (!logo || logo === 'undefined') {
123            return acc;
124         }
125
126         acc.push({
127            logo: logo,
128            website: member.Website || ''
129         });
130
131         return acc;
132      }, []);
133   }
134
135   /**
136    * DUPLICATE THE MEMBERS ARRAY IN SETS UNTIL WE HAVE ENOUGH SLIDES FOR
137    * SLICK TO SCROLL SMOOTHLY. TARGET IS 2x THE NUMBER OF VISIBLE SLOTS SO
138    * THERE IS ALWAYS A FULL OFF-SCREEN BUFFER ON BOTH SIDES OF THE VIEWPORT.
139    *
140    * @param  {Array}  members    VALID MEMBER OBJECTS.
141    * @param  {jQuery} $slider    THE SLIDER CONTAINER ELEMENT.
142    * @return {Array}             PADDED ARRAY.
143    */
144   function padSlidesToMinimum(members, $slider) {
145      var containerWidth = $slider.closest('.slider.inside').outerWidth() || $slider.outerWidth();
146      var visibleSlots = Math.ceil(containerWidth / SLOT_WIDTH_PX);
147      var minSlides = visibleSlots * 2;
148      var padded = members.slice();
149
150      while (padded.length < minSlides) {
151         padded = padded.concat(members);
152      }
153
154      return padded;
155   }
156
157   /**
158    * BUILD A SINGLE IMG ELEMENT FOR A MEMBER.
159    *
160    * @param  {Object} member  { logo, website }
161    * @return {jQuery}         JQUERY IMG ELEMENT.
162    */
163   function buildImg(member) {
164      var $img = $('<img>').attr({
165         src: member.logo,
166         alt: '' // DECORATIVE — MEANINGFUL ALT TEXT NOT AVAILABLE FROM API
167      });
168
169      if (member.website) {
170         // ENSURE THE URL HAS A PROTOCOL SO window.open NAVIGATES CORRECTLY
171         var href = /^https?:\/\//i.test(member.website)
172            ? member.website
173            : 'https://' + member.website;
174
175         $img.attr({
176            role: 'button',
177            tabindex: '0'
178         })
179            .css('cursor', 'pointer')
180            .on('click', function () {
181               window.open(href, '_blank', 'noopener,noreferrer');
182            })
183            .on('keypress', function (e) {
184               // KEYBOARD ACCESSIBILITY — ACTIVATE ON ENTER OR SPACE
185               if (e.which === 13 || e.which === 32) {
186                  window.open(href, '_blank', 'noopener,noreferrer');
187               }
188            });
189      }
190
191      return $img;
192   }
193
194   // ---------------------------------------------------------------------------
195   // DISPLAY MODE RESOLUTION
196   // ---------------------------------------------------------------------------
197
198   /**
199    * RESOLVE THE FINAL SCROLL/STATIC DECISION FOR A GIVEN INSTANCE.
200    *
201    * data-display-mode="scroll" → ALWAYS SCROLL
202    * data-display-mode="static" → ALWAYS STATIC
203    * data-display-mode="auto"   → MEASURE AND DECIDE
204    * MISSING ATTRIBUTE           → TREAT AS "auto"
205    *
206    * @param  {jQuery} $slider      THE SLIDER CONTAINER ELEMENT.
207    * @param  {number} memberCount  NUMBER OF VALID LOGOS FROM THE API.
208    * @return {boolean}             TRUE = SCROLL, FALSE = STATIC.
209    */
210   function shouldScroll($slider, memberCount) {
211      var mode = $slider.data('display-mode') || 'auto';
212
213      if (mode === 'scroll') {
214         return true;
215      }
216
217      if (mode === 'static') {
218         return false;
219      }
220
221      // AUTO — MEASURE CONTAINER VS TOTAL LOGO WIDTH
222      var containerWidth = $slider.closest('.slider.inside').outerWidth() || $slider.outerWidth();
223      var totalLogoWidth = memberCount * SLOT_WIDTH_PX;
224
225      return totalLogoWidth > containerWidth;
226   }
227
228   // ---------------------------------------------------------------------------
229   // RENDER
230   // ---------------------------------------------------------------------------
231
232   /**
233    * BUILD SLIDES AND INITIALIZE SLICK IN THE CORRECT MODE.
234    *
235    * SCROLL MODE: LOGOS ARE PADDED SO SLICK HAS A FULL CLONE BUFFER,
236    *              AUTOPLAY IS ON, INFINITE LOOP IS ON.
237    *
238    * STATIC MODE: NO PADDING. slidesToShow IS SET TO THE EXACT LOGO COUNT
239    *              SO ALL LOGOS ARE VISIBLE AT ONCE. AUTOPLAY AND INFINITE
240    *              ARE OFF. SLICK STILL INITIALIZES SO ALL CARD STYLES APPLY.
241    *
242    * @param {jQuery} $slider   THE SLIDER CONTAINER ELEMENT.
243    * @param {Array}  members   VALID MEMBER OBJECTS (UNPADDED).
244    */
245   function renderCarousel($slider, members) {
246      var scrolling = shouldScroll($slider, members.length);
247      var slides = scrolling ? padSlidesToMinimum(members, $slider) : members;
248      var $fragment = $(document.createDocumentFragment());
249
250      slides.forEach(function (member) {
251         $fragment.append(buildImg(member));
252      });
253
254      $slider.append($fragment);
255
256      // DEEP CLONE THE BASE CONFIG SO WE NEVER MUTATE THE SHARED OBJECT
257      var config = jQuery.extend(true, {}, SLICK_CONFIG);
258
259      if (!scrolling) {
260         // STATIC OVERRIDES — SHOW ALL LOGOS, NO MOTION, CENTERED
261         config.autoplay = false;
262         config.infinite = false;
263         config.cssEase = 'ease';
264         config.speed = 300;
265         config.slidesToShow = slides.length;
266         config.centerMode = false;
267         config.variableWidth = false;
268
269         // CENTER THE TRACK — ONLY WORKS ONCE variableWidth IS OFF
270         // SO SLICK CALCULATES A REAL TRACK WIDTH INSTEAD OF 45000px.
271         $slider.on('init', function (e, slick) {
272            $(slick.$list).css('text-align', 'center');
273            $(slick.$track).css({
274               'float': 'none',
275               'display': 'inline-block'
276            });
277         });
278      }
279
280      $slider.slick(config);
281   }
282
283   // ---------------------------------------------------------------------------
284   // INITIALIZATION — SUPPORTS MULTIPLE INSTANCES PER PAGE
285   // ---------------------------------------------------------------------------
286
287   /**
288    * INITIALIZE A SINGLE CAROUSEL INSTANCE.
289    *
290    * @param {jQuery} $slider  THE SLIDER CONTAINER ELEMENT.
291    * @param {string} apiUrl   THE API ENDPOINT URL FOR THIS INSTANCE.
292    */
293   function initCarousel($slider, apiUrl) {
294      fetchMembers(apiUrl)
295         .then(function (data) {
296            var validMembers = extractValidMembers(data);
297
298            if (validMembers.length === 0) {
299               console.warn('Members API Carousel: No valid logos returned.', apiUrl);
300               return;
301            }
302
303            renderCarousel($slider, validMembers);
304         })
305         .catch(function (err) {
306            console.error('Members API Carousel: Failed to load member data.', apiUrl, err);
307         });
308   }
309
310   /**
311    * FIND ALL CAROUSEL CONTAINERS ON THE PAGE AND INITIALIZE EACH ONE.
312    *
313    * API URL RESOLUTION ORDER PER INSTANCE:
314    *   1. data-api-url ATTRIBUTE ON THE ELEMENT  (PREFERRED — MULTI-INSTANCE SAFE)
315    *   2. membersCarouselData.apiUrl GLOBAL       (LEGACY wp_localize_script PATH)
316    *   3. members_url BARE GLOBAL                 (ORIGINAL INLINE SCRIPT PATH)
317    */
318   function init() {
319      $('.slider.right.members').each(function () {
320         var $slider = $(this);
321         var apiUrl = $slider.data('api-url') || '';
322
323         // LEGACY FALLBACKS — KEEP UN-UPDATED SITES WORKING
324         if (!apiUrl) {
325            if (typeof membersCarouselData !== 'undefined' && membersCarouselData.apiUrl) {
326               apiUrl = membersCarouselData.apiUrl;
327            } else if (typeof members_url !== 'undefined' && members_url) {
328               apiUrl = members_url;
329            }
330         }
331
332         if (!apiUrl) {
333            console.warn('Members API Carousel: No API URL found for instance.', this);
334            return;
335         }
336
337         initCarousel($slider, apiUrl);
338      });
339   }
340
341   init();
342
343});

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.