PageSourceSearch

https://www.heartmath.org/assets/plugins/hmi_products/assets/js/hmi_products.js?ver=1.0.3

js heartmath.org collected 2026-10-01 10:17:27 UTC 12,883 bytes, 479 lines download raw bytes

1(function ($) {
2
3/* ===============================
4   GLOBAL HELPER
5=============================== */
6
7function getQueryParam(param) {
8    const urlParams = new URLSearchParams(window.location.search);
9    let val = urlParams.get(param);
10
11    if (!val) return null;
12
13    val = val.toLowerCase().trim();
14
15    if (val === 'all') return null;
16
17    return val;
18}
19
20/* ===============================
21   SLIDER MODULE
22=============================== */
23
24function initSliders(scope = document) {
25
26    scope.querySelectorAll('.slider-track').forEach(track => {
27
28        if (track.dataset.init === '1') return;
29        track.dataset.init = '1';
30
31        const wrapper = track.closest('.slider-container');
32        const nextBtn = wrapper?.querySelector('.next');
33        const prevBtn = wrapper?.querySelector('.prev');
34        const dotsContainer = wrapper?.querySelector('.dots-container');
35        const controlsWrap = wrapper?.querySelector('.slider-dots');
36
37        let currentIndex = 0;
38        let slidesPerView = window.innerWidth <= 768 ? 1 : 3;
39
40        function getSlides() {
41            return Array.from(track.children).filter(el => $(el).is(':visible'));
42        }
43
44        function setupLayout() {
45            const slides = getSlides();
46            if (!slides.length) return;
47
48            const container = wrapper.querySelector('.hm-grid');
49            const containerWidth = container.clientWidth;
50
51            const gap = parseInt(window.getComputedStyle(track).gap) || 20;
52            const totalGap = gap * (slidesPerView - 1);
53
54            const slideWidth = (containerWidth - totalGap) / slidesPerView;
55
56            slides.forEach(slide => {
57                slide.style.flex = `0 0 ${slideWidth}px`;
58                slide.style.maxWidth = `${slideWidth}px`;
59            });
60        }
61
62        function createDots() {
63
64            if (!dotsContainer) return;
65
66            const slides = getSlides();
67            const totalSlides = slides.length;
68            const totalDots = Math.ceil(totalSlides / slidesPerView);
69
70            if (totalSlides <= slidesPerView) {
71                dotsContainer.innerHTML = '';
72                controlsWrap && (controlsWrap.style.display = 'none');
73                return;
74            } else {
75                controlsWrap && (controlsWrap.style.display = 'flex');
76            }
77
78            dotsContainer.innerHTML = '';
79
80            for (let i = 0; i < totalDots; i++) {
81                const dot = document.createElement('span');
82                dot.className = 'dot' + (i === 0 ? ' active' : '');
83
84                dot.addEventListener('click', () => {
85                    currentIndex = i;
86                    move();
87                });
88
89                dotsContainer.appendChild(dot);
90            }
91        }
92
93        function move() {
94            const slides = getSlides();
95            if (!slides.length) return;
96
97            const slide = slides[0];
98            const slideWidth = slide.offsetWidth;
99            const gap = parseInt(window.getComputedStyle(track).gap) || 0;
100
101            const moveX = currentIndex * (slideWidth + gap) * slidesPerView;
102
103            track.style.transform = `translateX(-${moveX}px)`;
104
105            if (dotsContainer) {
106                [...dotsContainer.children].forEach(d => d.classList.remove('active'));
107                dotsContainer.children[currentIndex]?.classList.add('active');
108            }
109        }
110
111        nextBtn?.addEventListener('click', () => {
112            const totalDots = Math.ceil(getSlides().length / slidesPerView);
113            if (currentIndex < totalDots - 1) {
114                currentIndex++;
115                move();
116            }
117        });
118
119        prevBtn?.addEventListener('click', () => {
120            if (currentIndex > 0) {
121                currentIndex--;
122                move();
123            }
124        });
125
126        setTimeout(() => {
127            setupLayout();
128            createDots();
129            move();
130        }, 80);
131
132        window.addEventListener('resize', () => {
133            slidesPerView = window.innerWidth <= 768 ? 1 : 3;
134            currentIndex = 0;
135
136            setupLayout();
137            createDots();
138            move();
139        });
140
141    });
142}
143
144/* ===============================
145   FILTER + SORT (SPONSORSHIP)
146=============================== */
147
148function sortByMeta($grid) {
149
150    let items = $grid.find('.resource-item:visible').get();
151
152    items.sort((a, b) => {
153        let oA = +a.dataset.order || 999999;
154        let oB = +b.dataset.order || 999999;
155        if (oA !== oB) return oA - oB;
156
157        let mA = +a.dataset.menuOrder || 0;
158        let mB = +b.dataset.menuOrder || 0;
159        if (mA !== mB) return mA - mB;
160
161        return (+b.dataset.date || 0) - (+a.dataset.date || 0);
162    });
163
164    $.each(items, (_, el) => $grid.append(el));
165}
166
167function applyFiltersAndSorting() {
168
169    $('.hm-sponsorship-wrapper').each(function () {
170
171        const $parent = $(this);
172
173        const category = ($parent.find('.sponsorship-hm-filter-dropdown').val() || 'all')
174            .toLowerCase().trim();
175
176        const $grid = $parent.find('.hm-grid');
177        const $items = $grid.find('.resource-item');
178
179        $items.each(function () {
180
181            const $card = $(this).find('.hm-card');
182
183            const terms = ($card.attr('data-terms') || '').toLowerCase();
184
185            const cats = ($card.attr('data-categories') || '')
186                .split(',')
187                .map(c => c.trim())
188                .filter(Boolean);
189
190            const show = (category === 'all') ? true : terms.includes('|' + category + '|');
191
192            $(this).toggle(show);
193
194            let display = cats[0] || '';
195
196            if (category !== 'all') {
197                const match = cats.find(c => c.toLowerCase() === category);
198                if (match) {
199                    display = match;
200                }
201            }
202
203            $card.find('.hm-cat').text(display);
204
205           
206
207        });
208
209        sortByMeta($grid);
210
211        const track = $grid[0];
212
213        if (track) {
214            track.style.transition = 'none';
215            track.style.transform = 'translateX(0)';
216            delete track.dataset.init;
217            track.offsetHeight;
218            track.style.transition = 'transform 0.3s ease';
219        }
220
221        setTimeout(() => {
222            initSliders($parent[0]);
223        }, 100);
224
225    });
226}
227
228/* ===============================
229   EVENTS (SPONSORSHIP)
230=============================== */
231
232$(document).on('change', '.sponsorship-hm-filter-dropdown', function () {
233
234    const val = $(this).val();
235    sessionStorage.setItem('hm_sponsorship_category', val);
236    applyFiltersAndSorting();
237});
238
239$(document).ready(function () {
240
241    let urlCategory = getQueryParam('category');
242    let savedCategory = sessionStorage.getItem('hm_sponsorship_category');
243
244    $('.hm-sponsorship-wrapper').each(function () {
245
246        const $dropdown = $(this).find('.sponsorship-hm-filter-dropdown');
247        let finalCategory = urlCategory || savedCategory;
248
249        if (finalCategory && $dropdown.find(`option[value="${finalCategory}"]`).length) {
250            $dropdown.val(finalCategory);
251        }
252    });
253
254    initSliders();
255
256    setTimeout(() => {
257        applyFiltersAndSorting();
258          if (urlCategory && $('.hm-resources').length) {
259            $('html, body').animate({
260                scrollTop: $('.hm-resources').offset().top
261            }, 800);
262        }
263    }, 50);
264});
265
266})(jQuery);
267
268/* ===============================
269   AJAX PRODUCTS
270=============================== */
271
272jQuery(function ($) {
273
274    let isLoading = false;
275    let $wrapper = $('.hm-resources');
276
277    let defaultType = $wrapper.data('default-type') || 'free';
278    let spanishCategory = $wrapper.data('spanish-category') || null;
279    let mergeFreePremium = $wrapper.data('merge-free-premium') == 1;
280    let switchedToPremiumForMerge = false;
281       
282    let type = defaultType;
283    let category = 'all';
284    let offset = 0;
285    let limit = 9;
286    let total = 0;
287
288    let urlCategory = (function () {
289        const urlParams = new URLSearchParams(window.location.search);
290        let val = urlParams.get('category');
291        return val ? val.toLowerCase().trim() : null;
292    })();
293
294    const $tabsContainer = $('.hm-tabs');
295    const $freeTab = $tabsContainer.find('[data-type="free"]');
296    const $premiumTab = $tabsContainer.find('[data-type="premium"]');
297    const $memberTab = $tabsContainer.find('[data-type="member"]');
298
299    if (defaultType === 'premium') {
300        $premiumTab.prependTo($tabsContainer);
301        $freeTab.appendTo($tabsContainer);
302         $memberTab.appendTo($tabsContainer);
303    } else {
304        $freeTab.prependTo($tabsContainer);
305        $premiumTab.appendTo($tabsContainer);
306        $memberTab.appendTo($tabsContainer);
307    }
308
309    $('.hm-tab-btn').removeClass('active');
310    $(`.hm-tab-btn[data-type="${type}"]`).addClass('active');
311
312    function loadCategories() {
313
314        $('#hm-category').html('<option>Loading...</option>');
315
316        $.post(hm_ajax_obj.ajax_url, {
317            action: 'hm_get_categories',            
318            type: type
319        }, function (res) {
320
321            let html = '<option value="all">All Categories</option>';
322
323            res.forEach(function (cat) {
324                html += `<option value="${cat.toLowerCase()}">${cat}</option>`;
325            });
326
327            $('#hm-category').html(html);
328
329            if (urlCategory && $('#hm-category option[value="' + urlCategory + '"]').length) {
330                category = urlCategory;
331                $('#hm-category').val(urlCategory);
332            }else if (spanishCategory && $('#hm-category option[value="' + spanishCategory.toLowerCase() + '"]').length) {
333                category = spanishCategory.toLowerCase();
334                $('#hm-category').val(spanishCategory.toLowerCase());
335            }
336
337            loadProducts(true);
338        });
339    }
340
341    function loadProducts(reset = false) {
342
343    if (isLoading) return;
344
345    isLoading = true;
346
347    if (reset) {
348
349        offset = 0;
350
351        $('#hm-products').html(`
352            <div class="hm-loader">
353                <span></span><span></span><span></span>
354            </div>
355        `);
356
357    }
358
359    $('#hm-load-more').hide();
360    $('#hm-scroll-loader').show();
361
362    $.post(hm_ajax_obj.ajax_url, {
363
364        action: 'hm_get_products',        
365        type: type,
366        category: category,
367        offset: offset,
368        is_spanish: spanishCategory ? 1 : 0
369
370    }, function (res) {
371
372        if (reset) {
373
374            $('#hm-products').html(res.html);
375
376        } else {
377
378            $('#hm-products').append(res.html);
379
380        }
381
382        total = res.total;
383
384        offset += limit;
385
386        isLoading = false;
387        $('#hm-scroll-loader').hide();
388
389    });
390
391}
392
393    /* INIT */
394    loadCategories();
395
396    /* EVENTS */
397
398  $(document).on('click', '.hm-tab-btn', function () {
399
400    $('.hm-tab-btn').removeClass('active');
401    $(this).addClass('active');
402
403    type = $(this).data('type');
404
405    $('#hm-mobile-tab-select').val(type);
406
407    category = 'all';
408    switchedToPremiumForMerge = false;
409
410    loadCategories();
411});
412
413$(document).on('change', '#hm-mobile-tab-select', function () {
414
415    let selectedType = $(this).val();
416
417    $('.hm-tab-btn').removeClass('active');
418
419    let $tab = $('.hm-tab-btn[data-type="' + selectedType + '"]');
420
421    $tab.addClass('active');
422
423    type = selectedType;
424    category = 'all';
425    switchedToPremiumForMerge = false;
426
427    loadCategories();
428});
429    $(document).on('change', '#hm-category', function () {
430        category = $(this).val();
431        loadProducts(true);
432    });
433
434   $(window).on('scroll', function () {
435
436    // already loading
437    if (isLoading) return;
438
439    let scrollTop = $(window).scrollTop();
440    let windowHeight = $(window).height();
441    let documentHeight = $(document).height();
442
443    // near bottom
444    let nearBottom = (scrollTop + windowHeight >= documentHeight - 600);
445
446    if (!nearBottom) return;
447
448    // all loaded for current type
449    if (offset >= total) {
450
451        // Spanish page special case: once free products are exhausted,
452        // keep scrolling in the same (free) tab but start loading premium products
453        // (still restricted to the "en español" category).
454        if (mergeFreePremium && type === 'free' && !switchedToPremiumForMerge) {
455
456            switchedToPremiumForMerge = true;
457            type = 'premium';
458            category = spanishCategory ? spanishCategory.toLowerCase() : 'all';
459            offset = 0;
460
461            let premiumLabel = $('.hm-tab-btn[data-type="premium"]').first().text().trim();
462            if (premiumLabel) {
463                $('#hm-products').append(
464                    $('<h2>', { class: 'hm-section-heading hm-merge-premium-heading', text: premiumLabel })
465                );
466            }
467
468            loadProducts(false);
469        }
470
471        return;
472    }
473
474    loadProducts(false);
475
476});
477
478    
479});

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.