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(/&/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.