PageSourceSearch

https://eusp.org/themes/EUSP2017/js/theme.js?tlt4j5

js eusp.org collected 2026-09-24 17:50:46 UTC 66,010 bytes, 1,601 lines download raw bytes

1(function ($, Drupal) {
2  "use strict";
3  Drupal.behaviors.myBehavior = {
4    weight: 50,
5    attach: function (context, settings) {
6
7      // Internet Explorer 6-11
8      var isIE = /*@cc_on!@*/ false || !!document.documentMode;
9
10
11      ////////////////////////////////////////////////////////////////////////
12      // add/remove 'shrinked-header' class on scroll
13
14      // $(window, context).once('shrink-header').each(function () {
15      //   const shrinkOn = 90; //$header-height - $shrinked-header-height
16      //   var shrinked = false;
17      //   //console.log(shrinked + ' ' + Date.now());
18
19      //   $(this).scroll(function () {
20      //     var curScrollPos = $(this).scrollTop();
21      //     if (curScrollPos > shrinkOn && !shrinked) {
22      //       $('#main-body').addClass("shrinked-header");
23      //       shrinked = true;
24      //       //console.log('sh+: ' + Date.now());
25      //     } else if (curScrollPos <= shrinkOn && shrinked) {
26      //       $('#main-body').removeClass("shrinked-header");
27      //       shrinked = false;
28      //       //console.log('sh-: ' + Date.now());
29      //     }
30      //   });
31      // });
32
33      ////////////////////////////////////////////////////////////////////////
34      // cookie 
35
36
37      function setCookie(name, value, days) {
38        var expires = "";
39        if (days) {
40          var date = new Date();
41          date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
42          expires = "; expires=" + date.toUTCString();
43        }
44        document.cookie = name + "=" + (value || "") + expires + "; path=/";
45      }
46
47      function getCookie(name) {
48        var nameEQ = name + "=";
49        var ca = document.cookie.split(';');
50        for (var i = 0; i < ca.length; i++) {
51          var c = ca[i];
52          while (c.charAt(0) == ' ') c = c.substring(1, c.length);
53          if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
54        }
55        return null;
56      }
57
58      function eraseCookie(name) {
59        document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
60      }
61
62      ////////////////////////////////////////////////////////////////////////
63      // color 
64
65      // var urlParams = new URLSearchParams(window.location.search);
66
67
68      // if (urlParams.has('color')) {
69      //   if (urlParams.get('color') == 1) {
70      //     console.log(getCookie('test_color'));
71      //     setCookie('test_color', 1, 3);
72      //     $('#main-body').removeClass('bw');
73      //   } else {
74      //     console.log('sf');
75      //     eraseCookie('test_color');
76      //   }
77      // } else {
78      //   if (getCookie('test_color')) {
79      //     $('#main-body').removeClass('bw');
80      //   }
81      // }
82
83      // if (getCookie('color_site')) {
84      //   $('#main-body').removeClass('bw');
85      // }
86
87      // $('.changeColor').click(function (e) {
88      //   e.preventDefault();
89      //   if (getCookie('color_site')) {
90      //     eraseCookie('color_site');
91      //     $('#main-body').addClass('bw');
92      //   } else {
93      //     $('#main-body').removeClass('bw');
94      //     setCookie('color_site', 1, 365);
95      //   }
96      //   ym(56495734,'reachGoal','color');
97      // });
98
99      ////////////////////////////////////////////////////////////////////////
100      // show/hide extended search bar 
101
102      $("#main-body", context).once("search-block").each(function () {
103        const search = $("#search-block");
104        var extended = false;
105        //console.log(extended + ' ' + Date.now());
106
107        $(this).click(function (e) {
108          // if the target of the click isn't the container
109          // and nor a descendant of the container
110          if (extended && !search.is(e.target) && search.has(e.target).length === 0) {
111            search.removeClass('extended');
112            extended = false;
113            //console.log('ext-: ' + Date.now());
114          }
115        });
116        search.click(function () {
117          if (!extended) {
118            $(this).addClass("extended");
119            $(this).find("input").focus();
120            extended = true;
121            //console.log('ext+: ' + Date.now());
122          }
123        });
124      });
125
126      ////////////////////////////////////////////////////////////////////////
127      // toggle-menu
128
129      $("#toggle-menu", context).once("toggle-menu").click(function () {
130        $("#header-nav-secondary, #mainmenu").toggleClass("show");
131        $(this).toggleClass("is-active");
132      });
133
134      ////////////////////////////////////////////////////////////////////////
135      // side-menu
136
137      $('.local-menu-wrapper', context).once('side-menu').click(function () {
138        $(this).toggleClass('collapsed');
139      });
140
141      ////////////////////////////////////////////////////////////////////////
142      // Smooth scrolling when clicking an anchor link
143
144      // $('a[href^="#"]').click(function () {
145      //   $('html, body').animate({
146      //       scrollTop: $('[name="' + $.attr(this, 'href').substr(1) + '"]').offset().top
147      //   }, 500);
148      //   return false;
149      // });
150
151      ////////////////////////////////////////////////////////////////////////
152      // 
153
154      $("#block-2fakultety," +
155        "#block-2departments," +
156        "#block-departments-ru-eng," +
157        "#block-menu-block-international," +
158        "#block-menu-block-international-en," +
159        "#block-menu-block-university," +
160        "#block-menu-university-en," +
161        "#block-menu-research," +
162        "#block-menu-research-en," +
163        "#block-3issledovaniya," +
164        "#block-3issledovaniya-en," +
165        "#block-menu-block-admissions," +
166        "#block-menu-block-additional," +
167        "#block-menu-block-additional-en", context
168      ).children(".menu-switcher").click(function () {
169        $(this).parent().children('.menu').toggleClass("show");
170      });
171
172      $("#block-main-events-filters," +
173        "#block-main-news-filters," +
174        "#block-main-announcements-filters," +
175        "#block-electronic-resources-filters," +
176        "#block-raskrytayaformasearchpage-1", context
177      ).children(".block-title").click(function () {
178        $(this).parent().children('.block-content').toggleClass("hidden");
179      });
180
181      function jump(h) {
182        var top = document.getElementById(h).offsetTop;
183        window.scrollTo(0, top);
184      }
185
186      ////////////////////////////////////////////////////////////////////////
187      // selectpiker auto-trigger
188
189      $('.selectpicker.auto-trigger', context).change(function () {
190        var currentValue = $(this).val();
191        var url = window.location.protocol + "//" + window.location.host + window.location.pathname
192        var urlParams = new URLSearchParams(window.location.search);
193        $(this).closest('form').find('[id^="edit-submit-"]').trigger('click');
194        urlParams.set('section', currentValue);
195        window.history.replaceState(null, null, url + '?' + urlParams);
196      });
197
198      // END: selectpiker auto-trigger
199      ////////////////////////////////////////////////////////////////////////
200
201      ////////////////////////////////////////////////////////////////////////
202      // News Rubrics to Title
203      var validURLs = ['/news', '/en/news'];
204      if (!isIE && $.inArray(window.location.pathname, validURLs) > -1) {
205
206        //console.log(window.location.pathname);
207
208        var titleID = '#euheader-section';
209        var baseTitle = $(titleID, context).find('h1').text();
210        var formID = '#views-exposed-form-news-main-news';
211        var submitButtonSelector = '[id^="edit-submit-news"]';
212        var dataSelector = formID + ' select[data-drupal-selector="edit-rubric"]';
213
214        //add initial rubric to title
215        $(titleID, context).each(function () {
216          var initialValue = $(dataSelector).val();
217          //console.log(initialValue);
218          if (initialValue != 'All') {
219            var additionalTitle = $(dataSelector).find('option:selected').text();
220            $(this).find('h1').text(baseTitle + ": " + additionalTitle);
221          }
222        });
223
224        // add rubric to title on chenge
225        $(dataSelector, context).change(function () {
226          var url = window.location.protocol + "//" + window.location.host + window.location.pathname
227          var urlParams = new URLSearchParams(window.location.search);
228          var additionalTitle = '';
229          var currentValue = $(this).val();
230          if (currentValue != 'All') {
231            additionalTitle = ': ' + $(this).find('option:selected').text();
232          }
233          $(titleID).find('h1').text(baseTitle + additionalTitle);
234          urlParams.set('rubric', currentValue);
235          window.history.replaceState(null, null, url + '?' + urlParams);
236          $(submitButtonSelector).trigger('click');
237        });
238
239      }
240      // END: News Rubrics to Title
241      ////////////////////////////////////////////////////////////////////////
242
243
244      ////////////////////////////////////////////////////////////////////////
245      // EUSP Online Views Exposed Filters
246      // Autosubmit on change any filters
247
248      if (isIE) {
249
250        $('#eu-online-controls', context).hide();
251        $('.view-euonline .view-filters').show();
252
253      } else {
254
255        if (window.location.pathname == '/online' || window.location.pathname == '/en/online') {
256
257          //console.log(window.location.pathname);
258
259          var url = window.location.protocol + "//" + window.location.host + window.location.pathname
260          var urlParams = new URLSearchParams(window.location.search);
261
262          //console.log('eee');
263
264          var euOnlineFormID = '#views-exposed-form-euonline-euonline-bydate';
265          var sortByOrigSelector = euOnlineFormID + ' select[data-drupal-selector="edit-sort-by"]';
266          var sectionOrigSelector = euOnlineFormID + ' select[data-drupal-selector="edit-section"]';
267          var sectionEnOrigSelector = euOnlineFormID + ' select[data-drupal-selector="edit-section-en"]';
268          var textOrigSelector = euOnlineFormID + ' input[data-drupal-selector="edit-text"]';
269          var submitButtonSelector = '[id^="edit-submit-euonline"]';
270
271          //apply initial values to custom filters elements
272          $('#eu-online-controls', context).each(function () {
273
274            var submit = false;
275
276            //console.log(context);
277
278            // custom buttons for sort by
279            var sortbyInitialValue = $(sortByOrigSelector).val();
280            if (urlParams.get('sort_by') && sortbyInitialValue != urlParams.get('sort_by')) {
281              var sortbyInitialValue = urlParams.get('sort_by');
282              $(sortByOrigSelector).val(sortbyInitialValue).change();
283              submit = true;
284            } else {
285              $('#' + sortbyInitialValue).parent().addClass('active');
286            }
287            console.log('sortby: ' + submit);
288
289            // custom select for sections
290            var sectionInitialValue = $(sectionOrigSelector).val();
291            if (urlParams.get('section') && sectionInitialValue != urlParams.get('section')) {
292              var sectionInitialValue = urlParams.get('section');
293              $(sectionOrigSelector).val(sectionInitialValue).change();
294              submit = true;
295            } else {
296              $('#eu-online-section-control').val(sectionInitialValue).change();
297            }
298            console.log('section: ' + submit);
299
300            // custom select for sections en
301            var sectionEnInitialValue = $(sectionEnOrigSelector).val();
302            if (urlParams.get('section_en') && sectionEnInitialValue != urlParams.get('section_en')) {
303              var sectionEnInitialValue = urlParams.get('section_en');
304              $(sectionEnOrigSelector).val(sectionEnInitialValue).change();
305              submit = true;
306            } else {
307              $('#eu-online-section-en-control').val(sectionEnInitialValue).change();
308            }
309            console.log('sectionEn: ' + submit);
310
311            // custom search for text search
312            var textInitialValue = $(textOrigSelector).val(),
313              textFromUrlParams = urlParams.get('text');
314            if (textFromUrlParams && textInitialValue != textFromUrlParams) {
315              $(textOrigSelector).val(textFromUrlParams);
316              submit = true;
317            } else {
318              $('#eu-online-text-control').val(textInitialValue).change();
319            }
320            console.log('text: ' + submit);
321
322            // submit initial values
323            if (submit) $(submitButtonSelector).trigger('click');
324          });
325
326          // Autosubmit: Sort By
327          $('#eu-online-sort-by-control  :input', context).change(function () {
328            var sortByCurrentValue = $(this).attr('id');
329            $(sortByOrigSelector).val(sortByCurrentValue).change();
330            $(submitButtonSelector).trigger('click');
331            urlParams.set('sort_by', sortByCurrentValue);
332            window.history.replaceState(null, null, url + '?' + urlParams);
333          })
334
335          // Autosubmit: Section
336          $('#eu-online-section-control', context).change(function () {
337            var sectionCurrentValue = $(this).val();
338            $(sectionOrigSelector).val(sectionCurrentValue).change();
339            $(submitButtonSelector).trigger('click');
340            urlParams.set('section', sectionCurrentValue);
341            window.history.replaceState(null, null, url + '?' + urlParams);
342          });
343
344          // Autosubmit: Section en
345          $('#eu-online-section-en-control', context).change(function () {
346            var sectionEnCurrentValue = $(this).val();
347            $(sectionEnOrigSelector).val(sectionEnCurrentValue).change();
348            $(submitButtonSelector).trigger('click');
349            urlParams.set('section_en', sectionEnCurrentValue);
350            window.history.replaceState(null, null, url + '?' + urlParams);
351          });
352
353          // Autosubmit: Text search
354          var thread = null;
355
356          function textSearch(textCurrentValue) {
357            $(textOrigSelector).val(textCurrentValue);
358            $(submitButtonSelector).trigger('click');
359            urlParams.set('text', textCurrentValue);
360            window.history.replaceState(null, null, url + '?' + urlParams);
361          }
362
363          $('#eu-online-text-control', context).on('keyup', function () {
364            clearTimeout(thread);
365            var $this = $(this);
366            thread = setTimeout(function () {
367              textSearch($this.val())
368            }, 1000);
369          });
370
371        }
372
373      }
374      // END: EUSP Online Views Exposed Filters
375      ////////////////////////////////////////////////////////////////////////
376
377
378      ////////////////////////////////////////////////////////////////////////
379      // Programs Views Exposed Filters
380      // Autosubmit on change any filters
381
382      if (isIE) {
383
384        $('#programs-controls', context).hide();
385        $('.view-programs .view-filters').show();
386
387      } else {
388
389        if (['/programs', '/programs/', '/en/programs', '/en/programs/', '/programs/cards'].includes(window.location.pathname)) {
390
391          //console.log(window.location.pathname);
392
393          var url = window.location.protocol + "//" + window.location.host + window.location.pathname
394          var urlParams = new URLSearchParams(window.location.search);
395
396          //console.log('eee');
397
398
399          // if (window.location.pathname == '/programs/cards') {
400          //   var programsFormID = '#views-exposed-form-programs-all-programs-cards';
401          // } else {
402          //   var programsFormID = '#views-exposed-form-programs-all-programs';
403          // }
404          var programsFormID = '#views-exposed-form-programs-all-programs-cards';
405          var plangOrigSelector = programsFormID + ' select[data-drupal-selector="edit-plang"]';
406          var typeOrigSelector = programsFormID + ' select[data-drupal-selector="edit-type"]';
407          var directionOrigSelector = programsFormID + ' select[data-drupal-selector="edit-direction"]';
408          var textOrigSelector = programsFormID + ' input[data-drupal-selector="edit-text"]';
409          var submitButtonSelector = '[id^="edit-submit-programs"]';
410
411          //apply initial values to custom filters elements
412          $('#programs-controls', context).each(function () {
413
414            var submit = false;
415
416            //console.log(context);
417
418            // custom buttons for plang
419            var plangInitialValue = $(plangOrigSelector).val();
420            if (urlParams.get('plang') && plangInitialValue != urlParams.get('plang')) {
421              var plangInitialValue = urlParams.get('plang');
422              $(plangOrigSelector).val(plangInitialValue).change();
423              submit = true;
424            } else {
425              $('#plang_' + plangInitialValue).parent().addClass('active');
426            }
427
428            // custom buttons for type
429            var typeInitialValue = $(typeOrigSelector).val();
430            if (urlParams.get('type') && typeInitialValue != urlParam
430s.get('type')) {
431              var typeInitialValue = urlParams.get('type');
432              $(typeOrigSelector).val(typeInitialValue).change();
433              submit = true;
434            } else {
435              $('#type_' + typeInitialValue).parent().addClass('active');
436            }
437
438            // custom select for direction
439            var directionInitialValue = $(directionOrigSelector).val();
440            if (urlParams.get('direction') && directionInitialValue != urlParams.get('direction')) {
441              var directionInitialValue = urlParams.get('direction');
442              $(directionOrigSelector).val(directionInitialValue).change();
443              submit = true;
444            } else {
445              $('#programs-direction-control').val(directionInitialValue).change();
446            }
447
448            // custom search for text search
449            var textInitialValue = $(textOrigSelector).val(),
450              textFromUrlParams = urlParams.get('text');
451            if (textFromUrlParams && textInitialValue != textFromUrlParams) {
452              $(textOrigSelector).val(textFromUrlParams);
453              submit = true;
454            } else {
455              $('#programs-text-control').val(textInitialValue).change();
456            }
457
458            // submit initial values
459            if (submit) $(submitButtonSelector).trigger('click');
460          });
461
462          // Autosubmit: plang
463          $('#plang-control  :input', context).change(function () {
464            var plangCurrentValue = $(this).attr('id').substring(6);
465            $(plangOrigSelector).val(plangCurrentValue).change();
466            $(submitButtonSelector).trigger('click');
467            urlParams.set('plang', plangCurrentValue);
468            window.history.replaceState(null, null, url + '?' + urlParams);
469          })
470
471          // Autosubmit: Type
472          $('#programs-type-control  :input', context).change(function () {
473            var typeCurrentValue = $(this).attr('id').substring(5);
474            $(typeOrigSelector).val(typeCurrentValue).change();
475            $(submitButtonSelector).trigger('click');
476            urlParams.set('type', typeCurrentValue);
477            window.history.replaceState(null, null, url + '?' + urlParams);
478          })
479
480          // Autosubmit: Section
481          $('#programs-direction-control', context).change(function () {
482            var directionCurrentValue = $(this).val();
483            $(directionOrigSelector).val(directionCurrentValue).change();
484            $(submitButtonSelector).trigger('click');
485            urlParams.set('direction', directionCurrentValue);
486            window.history.replaceState(null, null, url + '?' + urlParams);
487          });
488
489          // Autosubmit: Text search
490          var thread = null;
491
492          function textSearch(textCurrentValue) {
493            $(textOrigSelector).val(textCurrentValue);
494            $(submitButtonSelector).trigger('click');
495            urlParams.set('text', textCurrentValue);
496            window.history.replaceState(null, null, url + '?' + urlParams);
497          }
498
499          $('#programs-text-control', context).on('keyup', function () {
500            clearTimeout(thread);
501            var $this = $(this);
502            thread = setTimeout(function () {
503              textSearch($this.val())
504            }, 1000);
505          });
506
507        }
508
509      }
510      // END: Programs Views Exposed Filters
511      ////////////////////////////////////////////////////////////////////////
512
513
514      ////////////////////////////////////////////////////////////////////////
515      // EUSP Donation form
516
517      if (window.location.pathname == '/make-donation' || window.location.pathname == '/en/make-donation') {
518
519        $('.webform-submission-podderzhat-universitet-form').once('donation-form').each(function () {
520
521          var nameHidden = $(this).find('input[data-drupal-selector="edit-name-hidden"]', context);
522          var name = $(this).find('input[data-drupal-selector="edit-name"]', context);
523
524          var amountHidden = $(this).find('input[data-drupal-selector="edit-amount-hidden"]', context);
525          var amount = $(this).find('select[data-drupal-selector="edit-amount-select"]', context);
526          var amountOther = $(this).find('input[data-drupal-selector="edit-amount-other"]', context);
527
528          var targetHidden = $(this).find('input[data-drupal-selector="edit-target-hidden"]', context);
529          var target = $(this).find('select[data-drupal-selector="edit-target"]', context);
530
531          var recurrentHidden = $(this).find('input[data-drupal-selector="edit-recurrent-hidden"]', context);
532          var recurrent = $(this).find('input[data-drupal-selector="edit-recurrent"]', context);
533
534          var emailHidden = $(this).find('input[data-drupal-selector="edit-donor-email-hidden"]', context);
535          var email = $(this).find('input[data-drupal-selector="edit-donor-email"]', context);
536
537          var iamHidden = $(this).find('input[data-drupal-selector="edit-i-am-hidden"]', context);
538          var iam = $(this).find('select[data-drupal-selector="edit-i-am"]', context);
539
540          var donationCompleted = $(this).find('input[data-drupal-selector="edit-donation-is-completed"]', context);
541
542          var step = $(this).find('.form-wrapper[data-webform-key]', context);
543          var stepValue = '';
544          if (step.length > 0) stepValue = step.data('webform-key');
545          console.log('Step: ' + stepValue);
546
547          ////////////////////////////////////////////////////
548          // 1st step: donation form
549          if (stepValue == 'donation_form') {
550
551            /////////////////// Name
552            nameHidden.val(name.val()); //console.log(nameHidden.val());
553            name.on('focusout', function () {
554              nameHidden.val($(this).val());
555            });
556
557            /////////////////// Amount
558            var newAmountValue = amount.val();
559            if (newAmountValue != '_other_') {
560              amountHidden.val(newAmountValue);
561              amountOther.val(newAmountValue);
562              //console.log(amountHidden.val());
563            }
564            amount.change(function () {
565              newAmountValue = $(this).val();
566              if (newAmountValue != '_other_') amountHidden.val(newAmountValue);
567            });
568            amountOther.on('focusout', function () {
569              newAmountValue = $(this).val();
570              if (newAmountValue && newAmountValue >= 100) amountHidden.val(newAmountValue);
571            });
572
573            /////////////////// Target
574            targetHidden.val(target.val()); //console.log(targetHidden.val());
575            target.change(function () {
576              targetHidden.val($(this).val());
577            });
578
579            /////////////////// Recurrent?
580            recurrentHidden.val(recurrent.is(":checked")); //console.log(recurrentHidden.val());
581            recurrent.change(function () {
582              recurrentHidden.val($(this).is(":checked"));
583              console.log(recurrentHidden.val());
584            });
585
586            /////////////////// e-mail
587            emailHidden.val(email.val()); //console.log(emailHidden.val());
588            email.on('focusout', function () {
589              emailHidden.val($(this).val());
590            });
591
592            /////////////////// I am
593            iamHidden.val(iam.val()); //console.log(iamHidden.val());
594            iam.change(function () {
595              iamHidden.val($(this).val());
596            });
597
598            ////////////////////////////////////////////////////
599            // 2nd step: payment
600          } else if (stepValue == 'payment') {
601
602            //console.log(Date.now());
603
604            function donation() {
605              var widget = new cp.CloudPayments();
606              var data = {
607                name: nameHidden.val(),
608                donor: iamHidden.val()
609              };
610              var amountValue = parseFloat(amountHidden.val());
611              var target = "Пожертвование на поддержку: " + targetHidden.val();
612              var accountId = emailHidden.val();
613              if (recurrentHidden.val() == 'true') {
614                data.cloudPayments = {
615                  recurrent: {
616                    interval: 'Month',
617                    period: 1
618                  }
619                }
620                console.log(Date.now());
621              }
622              widget.charge({
623                  publicId: 'pk_4b122180185ad23bba1fbfe2cd9c7',
624                  description: target,
625                  amount: amountValue,
626                  currency: 'RUB',
627                  accountId: accountId,
628                  email: accountId,
629                  data: data
630                },
631                function (options) { // success
632                  //действие при успешной оплате
633                  jump('donation-bottom');
634                  jump('main-content');
635                  $('input[data-drupal-selector="edit-donation-is-completed"]', context).prop("checked", true);
636                  $('[id^="edit-actions-submit"]', context).trigger('click');
637                  console.log('success');
638                },
639                function (reason, options) { // fail
640                  //действие при неуспешной оплате
641                  console.log(reason);
642                });
643            }
644
645            donation();
646
647          }
648
649        });
650
651      }
652
653      // END: EUSP Donation form
654      ////////////////////////////////////////////////////////////////////////
655
656
657      ////////////////////////////////////////////////////////////////////////
658      // VDNKH temp
659
660      // if (window.location.pathname == '/events/xiv-obscherossiyskaya-nauchnaya-konferenciya-vdnkh' || window.location.pathname == '/events/xiv-obscherossiyskaya-nauchnaya-konferenciya-vdnkh/') {
661      //   setTimeout(function(){
662      //     $('div.ckeditor-accordion-container', context).each(function () {
663      //       $(this).find('dt.active').removeClass('active');
664      //       $(this).find('dd.active').removeClass('active').hide();
665      //     });
666      //   }, 1000);
667      // }
668
669      // END: VDNKH temp
670      ////////////////////////////////////////////////////////////////////////
671
672
673      ////////////////////////////////////////////////////////////////////////
674      // Main with Side Slideshow synchronization
675      
676      $('.slideshow.main-with-side', context).once('main-with-side-slideshow').each(function() {
677        var $wrapper = $(this);
678        var $mainSlider = $wrapper.find('.slideshow-main .slick-slider');
679        var $sideContainer = $wrapper.find('.slideshow-side');
680        var $sideElements = $sideContainer.find('.slideshow-element');
681        
682        // Ждем инициализации главного Slick слайдера
683        var checkSlick = setInterval(function() {
684          if ($mainSlider.hasClass('slick-initialized')) {
685            clearInterval(checkSlick);
686            initMainWithSideSlideshow($mainSlider, $sideContainer, $sideElements);
687          }
688        }, 100);
689      });
690
691      function initMainWithSideSlideshow($mainSlider, $sideContainer, $sideElements) {
692        // Устанавливаем первый слайд как активный при загрузке
693        $sideElements.first().addClass('active');
694        
695        // Синхронизация: клик на боковом слайде переключает главный
696        $sideElements.on('click', function(e) {
697          e.preventDefault();
698          var slideIndex = $(this).index();
699          if (slideIndex !== undefined && slideIndex >= 0) {
700            $mainSlider.slick('slickGoTo', slideIndex);
701          }
702        });
703
704        // Синхронизация: при изменении главного слайда обновляем активный боковой
705        $mainSlider.on('afterChange', function(event, slick, currentSlide) {
706          $sideElements.removeClass('active');
707          $sideElements.eq(currentSlide).addClass('active');
708        });
709      }
710
711      // END: Main with Side Slideshow synchronization
712      ////////////////////////////////////////////////////////////////////////
713
714      ////////////////////////////////////////////////////////////////////////
715      // Main with Side Slideshow for View (FlexSlider)
716      
717      $('.slideshow-block.main-with-side', context).once('main-with-side-view-slideshow').each(function() {
718        var $wrapper = $(this);
719        var $flexslider = $wrapper.find('.flexslider');
720
721        $flexslider.on('init', function() {
722          var $slider = $(this);
723          var flexslider = $slider.data('flexslider');
724          stopFlexsliderAutoplay(flexslider);
725          resetFlexsliderSlideStyles($slider);
726        });
727
728        var checkFlexslider = setInterval(function() {
729          if ($flexslider.data('flexslider')) {
730            clearInterval(checkFlexslider);
731            initMainWithSideViewSlideshow($flexslider);
732          }
733        }, 10);
734      });
735
736      function stopFlexsliderAutoplay(flexslider) {
737        if (!flexslider) {
738          return;
739        }
740        if (typeof flexslider.pause === 'function') {
741          flexslider.pause();
742        }
743        if (flexslider.animatedSlides) {
744          clearInterval(flexslider.animatedSlides);
745          flexslider.animatedSlides = false;
746        }
747        if (flexslider.vars) {
748          flexslider.vars.slideshow = false;
749        }
750        if (typeof flexslider.animating !== 'undefined') {
751          flexslider.animating = false;
752        }
753      }
754
755      function resetFlexsliderSlideStyles($flexslider) {
756        $flexslider.find('.slides > li').each(function() {
757          $(this).css({
758            marginRight: '',
759            float: '',
760            width: '',
761            opacity: '',
762            zIndex: '',
763            display: '',
764            position: '',
765            left: '',
766            top: ''
767          });
768        });
769      }
770
771      function initMainWithSideViewSlideshow($flexslider) {
772        if ($flexslider.data('mainWithSideReady')) {
773          return;
774        }
775        $flexslider.data('mainWithSideReady', true);
776
777        var $wrapper = $flexslider.closest('.slideshow-block.main-with-side');
778        var flexslider = $flexslider.data('flexslider');
779        stopFlexsliderAutoplay(flexslider);
780
781        var $slidesContainer = $flexslider.find('.slides');
782        var slidesArray = $slidesContainer.find('> li').toArray();
783        var totalSlides = slidesArray.length;
784        if (!totalSlides) {
785          return;
786        }
787
788        slidesArray.forEach(function(slide, i) {
789          $(slide).attr('data-original-index', i);
790        });
791
792        var currentIndex = 0;
793        var autoRotateInterval = null;
794        var slideDuration = 15000;
795        var mobileQuery = window.matchMedia('(max-width: 768px)');
796        var $mobileControls = null;
797        var touchStartX = 0;
798        var touchStartY = 0;
799
800        function isMobile() {
801          return mobileQuery.matches;
802        }
803
804        function updateSlidesDisplay() {
805          var mainIndex = currentIndex % totalSlides;
806          var side1Index = (currentIndex + 1) % totalSlides;
807          var side2Index = (currentIndex + 2) % totalSlides;
808
809          var orderedSlides = [
810            slidesArray[mainIndex],
811            side1Index !== mainIndex ? slidesArray[side1Index] : null,
812            side2Index !== mainIndex && side2Index !== side1Index ? slidesArray[side2Index] : null
813          ].filter(function(slide) {
814            return slide !== null;
815          });
816
817          // prepend в обратном порядке: main, side1, side2 оказываются первыми в DOM
818          // (append отправлял их в конец, и CSS :nth-child(1-3) показывал не те слайды)
819          for (var i = orderedSlides.length - 1; i >= 0; i--) {
820            $slidesContainer.prepend(orderedSlides[i]);
821          }
822
823          resetFlexsliderSlideStyles($flexslider);
824          updateMobileNav();
825        }
826
827        function goToSlide(index) {
828          if (index < 0 || index >= totalSlides || index === currentIndex) {
829            return;
830          }
831          currentIndex = index;
832          updateSlidesDisplay();
833          restartAutoplay();
834        }
835
836        function nextSlide() {
837          goToSlide((currentIndex + 1) % totalSlides);
838        }
839
840        function prevSlide() {
841          goToSlide((currentIndex - 1 + totalSlides) % totalSlides);
842        }
843
844        function restartAutoplay() {
845          if (autoRotateInterval) {
846            clearInterval(autoRotateInterval);
847          }
848          autoRotateInterval = setInterval(nextSlide, slideDuration);
849        }
850
851        function updateMobileNav() {
852          if (!$mobileControls || !$mobileControls.length) {
853            return;
854          }
855          $mobileControls.find('.slideshow-mobile-dots button')
856            .removeClass('is-active')
857            .attr('aria-selected', 'false')
858            .eq(currentIndex)
859            .addClass('is-active')
860            .attr('aria-selected', 'true');
861        }
862
863        function syncMobileControlsVisibility() {
864          if (!$mobileControls || !$mobileControls.length) {
865            return;
866          }
867          var showControls = isMobile() && totalSlides > 1;
868          $mobileControls.toggle(showControls);
869          $flexslider.toggleClass('has-mobile-swipe', showControls);
870        }
871
872        function buildMobileNav() {
873          if (totalSlides <= 1 || $wrapper.find('.slideshow-mobile-controls').length) {
874            return;
875          }
876
877          $mobileControls = $('<div class="slideshow-mobile-controls" aria-label="Навигация по баннерам"></div>');
878          var $dots = $('<ol class="slideshow-mobile-dots" role="tablist"></ol>');
879
880          for (var i = 0; i < totalSlides; i++) {
881            $dots.append(
882              $('<li role="presentation"><button type="button" role="tab" aria-label="Слайд ' + (i + 1) + '" data-slide-index="' + i + '"></button></li>')
883            );
884          }
885
886          $mobileControls.append($dots);
887          $wrapper.append($mobileControls);
888
889          $dots.on('click', 'button', function(e) {
890            e.preventDefault();
891            var idx = parseInt($(this).attr('data-slide-index'), 10);
892            if (!isNaN(idx)) {
893              goToSlide(idx);
894            }
895          });
896
897          syncMobileControlsVisibility();
898          updateMobileNav();
899        }
900
901        $flexslider.on('click', '.slides > li:nth-child(2), .slides > li:nth-child(3)', function(e) {
902          e.preventDefault();
903          var clickedIndex = parseInt($(this).attr('data-original-index'), 10);
904          if (!isNaN(clickedIndex)) {
905            goToSlide(clickedIndex);
906          }
907        });
908
909        $flexslider.on('touchstart', function(e) {
910          if (!isMobile() || totalSlides <= 1) {
911            return;
912          }
913          touchStartX = e.originalEvent.touches[0].clientX;
914          touchStartY = e.originalEvent.touches[0].clientY;
915        });
916
917        $flexslider.on('touchend', function(e) {
918          if (!isMobile() || totalSlides <= 1) {
919            return;
920          }
921          var touch = e.originalEvent.changedTouches[0];
922          var deltaX = touch.clientX - touchStartX;
923          var deltaY = touch.clientY - touchStartY;
924          if (Math.abs(deltaX) < 50 || Math.abs(deltaX) < Math.abs(deltaY)) {
925            return;
926          }
927          if (deltaX < 0) {
928            nextSlide();
929          } else {
930            prevSlide();
931          }
932        });
933
934        if (typeof mobileQuery.addEventListener === 'function') {
935          mobileQuery.addEventListener('change', syncMobileControlsVisibility);
936        } else if (typeof mobileQuery.addListener === 'function') {
937          mobileQuery.addListener(syncMobileControlsVisibility);
938        }
939
940        buildMobileNav();
941        updateSlidesDisplay();
942        $flexslider.addClass('main-with-side-initialized');
943        $wrapper.addClass('main-with-side-ready');
944        restartAutoplay();
945
946        $flexslider.on('mouseenter', function() {
947          if (autoRotateInterval) {
948            clearInterval(autoRotateInterval);
949            autoRotateInterval = null;
950          }
951        });
952
953        $flexslider.on('mouseleave', function() {
954          restartAutoplay();
955        });
956      }
957
958      // END: Main with Side Slideshow for View (FlexSlider)
959      ////////////////////////////////////////////////////////////////////////
960
961      // Programs Carousel - теперь инициализируется через модуль Slick в админке
962      // JavaScript инициализация больше не нужна, так как модуль Slick делает это автоматически
963      ////////////////////////////////////////////////////////////////////////
964
965      // Program filters: reset button and AJAX handling
966      ////////////////////////////////////////////////////////////////////////
967      $(context).find('.program-filters').once('programFilters').each(function() {
968        var $filters = $(this);
969        var $form = $filters.closest('form');
970        
971        // Кнопка сброса фильтра
972        $filters.find('.filter-reset-btn').on('click', function(e) {
973          e.preventDefault();
974          
975          // Сбрасываем все чекбоксы/радиокнопки
976          $filters.find('input[type="checkbox"]:checked, input[type="radio"]:checked').prop('checked', false);
977          
978          // Если есть select, сбрасываем его
979          $filters.find('select').val('').trigger('change');
980          
981          // Сбрасываем кнопки select (если они были созданы)
982          $filters.find('.filter-button-select').removeClass('active');
983          
984          // Отправляем форму
985          if ($form.length) {
986            // Если AJAX включен, используем его
987            if ($form.hasClass('views-ajax-form') || $form.data('ajax-submit')) {
988              $form.trigger('submit');
989            } else {
990              // Иначе обычная отправка
991              $form.submit();
992            }
993          }
994        });
995        
996        // Обработка клика по фильтрам без перезагрузки (если AJAX включен)
997        $filters.find('input[type="checkbox"], input[type="radio"]').on('change', function() {
998          if ($form.hasClass('views-ajax-form') || $form.data('ajax-submit')) {
999            // Небольшая задержка для AJAX
1000            clearTimeout(window.filterTimeout);
1001            window.filterTimeout = setTimeout(function() {
1002              $form.trigger('submit');
1003            }, 300);
1004          }
1005        });
1006      });
1007      
1008      // Convert select to buttons for program filters
1009      ////////////////////////////////////////////////////////////////////////
1010      $(context).find('.program-filters select').once('convertSelectToButtons').each(function() {
1011        var $select = $(this);
1012        var $container = $select.closest('.filter-buttons');
1013        
1014        if ($container.length === 0) {
1015          $container = $select.parent();
1016        }
1017        
1018        // Create buttons container
1019        var $buttonsContainer = $('<div class="select-buttons"></div>');
1020        
1021        // Create button for each option
1022        $select.find('option').each(function() {
1023          var $option = $(this);
1024          var value = $option.val();
1025          var text = $option.text();
1026          
1027          if (value === '' || value === '_none') {
1028            return; // Skip empty option
1029          }
1030          
1031          var $button = $('<button type="button" class="filter-button-select" data-value="' + value + '">' + text + '</button>');
1032          
1033          // Mark as active if selected
1034          if ($option.prop('selected')) {
1035            $button.addClass('active');
1036          }
1037          
1038          // Click handler
1039          $button.on('click', function(e) {
1040            e.preventDefault();
1041            
1042            // Update select
1043            $select.val(value).trigger('change');
1044            
1045            // Update button states
1046            $buttonsContainer.find('.filter-button-select').removeClass('active');
1047            $button.addClass('active');
1048            
1049            // Submit form (with AJAX if available)
1050            var $form = $select.closest('form');
1051            if ($form.hasClass('views-ajax-form') || $form.data('ajax-submit')) {
1052              $form.trigger('submit');
1053            } else {
1054              $form.submit();
1055            }
1056          });
1057          
1058          $buttonsContainer.append($button);
1059        });
1060        
1061        // Hide select and show buttons
1062        $select.hide().after($buttonsContainer);
1063      });
1064      // END: Convert select to buttons
1065      ////////////////////////////////////////////////////////////////////////
1066
1067      ////////////////////////////////////////////////////////////////////////
1068      // Horizontal scroll with mouse wheel for programs carousel
1069      ////////////////////////////////////////////////////////////////////////
1070      $(context).find('.programs-carousel:not(.slick-initialized)').once('programsCarouselWheel').each(function() {
1071        var $carousel = $(this);
1072        
1073        // Horizontal scroll with mouse wheel
1074        $carousel.on('wheel', function(e) {
1075          // Only if not at the edges
1076          var scrollLeft = this.scrollLeft;
1077          var scrollWidth = this.scrollWidth;
1078          var clientWidth = this.clientWidth;
1079          
1080          // Check if we can scroll in the direction of the wheel
1081          if ((e.originalEvent.deltaY > 0 && scrollLeft < scrollWidth - clientWidth) ||
1082              (e.originalEvent.deltaY < 0 && scrollLeft > 0)) {
1083            e.preventDefault();
1084            this.scrollLeft += e.originalEvent.deltaY;
1085          }
1086        });
1087      });
1088      // END: Horizontal scroll with mouse wheel
1089      ////////////////////////////////////////////////////////////////////////
1090
1091      ////////////////////////////////////////////////////////////////////////
1092      // Scroll affordance for homepage program carousels (desktop gradient)
1093      ////////////////////////////////////////////////////////////////////////
1094      $(context).find('.programs-main-section .view-content').once('programsScrollAffordance').each(function() {
1095        var $container = $(this);
1096        var $section = $container.closest('.programs-main-section');
1097        var $carousel = $container.find('.programs-carousel:not(.slick-initialized)');
1098
1099        if ($carousel.length === 0) {
1100          return;
1101        }
1102
1103        function updateScrollState() {
1104          var el = $carousel[0];
1105          var scrollLeft = el.scrollLeft;
1106          var scrollWidth = el.scrollWidth;
1107          var clientWidth = el.clientWidth;
1108          var atEnd = scrollLeft >= scrollWidth - clientWidth - 5;
1109          var canScroll = scrollWidth > clientWidth + 5;
1110
1111          $section.toggleClass('is-scrolled-end', atEnd);
1112          $section.toggleClass('has-scroll', canScroll);
1113        }
1114
1115        setTimeout(updateScrollState, 100);
1116        $carousel.on('scroll', updateScrollState);
1117        $(window).on('resize', updateScrollState);
1118      });
1119      // END: Scroll affordance
1120      ////////////////////////////////////////////////////////////////////////
1121
1122      ////////////////////////////////////////////////////////////////////////
1123      // Navigation arrows for programs carousel (desktop) - динамические стрелки
1124      // Применяем ко всем блокам программ: магистратура, аспирантура, ДПО
1125      ////////////////////////////////////////////////////////////////////////
1126      $(context).find('.programs-main-section .view-content, .program-section .view-content, .view-programmy .view-content').once('programsCarouselArrows').each(function() {
1127        var $container = $(this);
1128
1129        // На главной используем кнопку «смотреть больше» вместо стрелок.
1130        if ($container.closest('.programs-main-section').length > 0) {
1131          return;
1132        }
1133
1134        var $carousel = $container.find('.programs-carousel:not(.slick-initialized)');
1135        var $slickCarousel = $container.find('.slick-slider');
1136        
1137        // Обработка нативного скролла (без Slick)
1138        if ($carousel.length > 0) {
1139          // Create arrows if they don't exist
1140          if ($container.find('.carousel-nav-arrow').length === 0) {
1141            var $prevArrow = $('<button type="button" class="carousel-nav-arrow carousel-nav-prev" aria-label="Предыдущие"><span>‹</span></button>');
1142            var $nextArrow = $('<button type="button" class="carousel-nav-arrow carousel-nav-next" aria-label="Следующие"><span>›</span></button>');
1143            
1144            $container.css('position', 'relative');
1145            $container.append($prevArrow);
1146            $container.append($nextArrow);
1147            
1148            // Update arrow visibility based on scroll position
1149            function updateArrows() {
1150              var scrollLeft = $carousel[0].scrollLeft;
1151              var scrollWidth = $carousel[0].scrollWidth;
1152              var clientWidth = $carousel[0].clientWidth;
1153              
1154              // Показываем левую стрелку только если можно прокрутить влево
1155              if (scrollLeft > 5) {
1156                $prevArrow.addClass('show');
1157              } else {
1158                $prevArrow.removeClass('show');
1159              }
1160              
1161              // Показываем правую стрелку только если можно прокрутить вправо
1162              if (scrollLeft < scrollWidth - clientWidth - 5) {
1163                $nextArrow.addClass('show');
1164              } else {
1165                $nextArrow.removeClass('show');
1166              }
1167            }
1168            
1169            // Initial check
1170            setTimeout(updateArrows, 100);
1171            
1172            // Update on scroll
1173            $carousel.on('scroll', updateArrows);
1174            
1175            // Update on resize
1176            $(window).on('resize', updateArrows);
1177            
1178            // Click handlers
1179            $prevArrow.on('click', function() {
1180              $carousel.animate({
1181                scrollLeft: '-=300'
1182              }, 300);
1183            });
1184            
1185            $nextArrow.on('click', function() {
1186              $carousel.animate({
1187                scrollLeft: '+=300'
1188              }, 300);
1189            });
1190          }
1191        }
1192        
1193        // Обработка Slick Carousel
1194        if ($slickCarousel.length > 0 && typeof $.fn.slick !== 'undefined') {
1195          // Скрываем стандартные стрелки Slick и создаем свои
1196          $slickCarousel.find('.slick-prev, .slick-next').css({
1197            'display': 'none !important',
1198            'visibility': 'hidden',
1199            'opacity': '0'
1200          });
1201          
1202          if ($container.find('.carousel-nav-arrow').length === 0) {
1203            var $prevArrow = $('<button type="button" class="carousel-nav-arrow carousel-nav-prev" aria-label="Предыдущие"><span>‹</span></button>');
1204            var $nextArrow = $('<button type="button" class="carousel-nav-arrow carousel-nav-next" aria-label="Следующие"><span>›</span></button>');
1205            
1206            $container.css('position', 'relative');
1207            $container.append($prevArrow);
1208            $container.append($nextArrow);
1209            
1210            function updateSlickArrows() {
1211              try {
1212                var currentSlide = $slickCarousel.slick('slickCurrentSlide') || 0;
1213                var slideCount = $slickCarousel.slick('getOption', 'slidesToShow') || 4;
1214                var totalSlides = $slickCarousel.find('.slick-slide:not(.slick-cloned)').length;
1215                
1216                // Показываем левую стрелку только если не на первом слайде
1217                if (currentSlide > 0) {
1218                  $prevArrow.addClass('show');
1219                } else {
1220                  $prevArrow.removeClass('show');
1221                }
1222                
1223                // Показываем правую стрелку только если не на последнем слайде
1224                var canGoNext = currentSlide < totalSlides - slideCount;
1225                if (canGoNext) {
1226                  $nextArrow.addClass('show');
1227                } else {
1228                  $nextArrow.removeClass('show');
1229                }
1230              } catch(e) {
1231                console.log('Error updating Slick arrows:', e);
1232              }
1233            }
1234            
1235            // Update on Slick events
1236            $slickCarousel.on('afterChange', updateSlickArrows);
1237            $slickCarousel.on('init', updateSlickArrows);
1238            $slickCarousel.on('reInit', updateSlickArrows);
1239            
1240            // Initial check - несколько попыток, так как Slick может инициализироваться позже
1241            setTimeout(updateSlickArrows, 200);
1242            setTimeout(updateSlickArrows, 500);
1243            setTimeout(updateSlickArrows, 1000);
1244            
1245            // Click handlers
1246            $prevArrow.on('click', function() {
1247              $slickCarousel.slick('slickPrev');
1248            });
1249            
1250            $nextArrow.on('click', function() {
1251              $slickCarousel.slick('slickNext');
1252            });
1253          }
1254        }
1255      });
1256      // END: Navigation arrows
1257      ////////////////////////////////////////////////////////////////////////
1258
1259      ////////////////////////////////////////////////////////////////////////
1260      // Navigation arrows for alumni carousel (desktop)
1261      ////////////////////////////////////////////////////////////////////////
1262      $(context).find('.alumni-carousel-wrapper').once('alumniCarouselArrows').each(function() {
1263        var $container = $(this);
1264        var $carousel = $container.find('.alumni-carousel');
1265        
1266        if ($carousel.length === 0) return;
1267        
1268        // Create arrows if they don't exist
1269        if ($container.find('.alumni-carousel-nav').length === 0) {
1270          var $prevArrow = $('<button type="button" class="alumni-carousel-nav alumni-carousel-nav-prev" aria-label="Предыдущие"><span>‹</span></button>');
1271          var $nextArrow = $('<button type="button" class="alumni-carousel-nav alumni-carousel-nav-next" aria-label="Следующие"><span>›</span></button>');
1272          
1273          $container.css('position', 'relative');
1274          $container.append($prevArrow);
1275          $container.append($nextArrow);
1276          
1277          // Update arrow visibility based on scroll position
1278          function updateArrows() {
1279            var scrollLeft = $carousel[0].scrollLeft;
1280            var scrollWidth = $carousel[0].scrollWidth;
1281            var clientWidth = $carousel[0].clientWidth;
1282            
1283            $prevArrow.toggle(scrollLeft > 0);
1284            $nextArrow.toggle(scrollLeft < scrollWidth - clientWidth - 1);
1285          }
1286          
1287          // Initial check
1288          updateArrows();
1289          
1290          // Update on scroll
1291          $carousel.on('scroll', updateArrows);
1292          
1293          // Update on resize
1294          $(window).on('resize', updateArrows);
1295          
1296          // Click handlers
1297          $prevArrow.on('click', function() {
1298            $carousel.animate({
1299              scrollLeft: '-=280'
1300            }, 300);
1301          });
1302          
1303          $nextArrow.on('click', function() {
1304            $carousel.animate({
1305              scrollLeft: '+=280'
1306            }, 300);
1307          });
1308        }
1309      });
1310      // END: Alumni carousel navigation arrows
1311      ////////////////////////////////////////////////////////////////////////
1312
1313      ////////////////////////////////////////////////////////////////////////
1314      // Navigation dots for alumni carousel
1315      ////////////////////////////////////////////////////////////////////////
1316      $(context).find('.alumni-carousel-wrapper').once('alumniCarouselDots').each(function() {
1317        var $container = $(this);
1318        var $carousel = $container.find('.alumni-carousel');
1319        
1320        if ($carousel.length === 0) return;
1321        
1322        var $alumniItems = $carousel.find('.alumni');
1323        if ($alumniItems.length === 0) return;
1324        
1325        // Calculate how many items are visible at once
1326        function getVisibleCount() {
1327          var carouselWidth = $carousel[0].clientWidth;
1328          var itemWidth = $alumniItems.first().outerWidth(true);
1329          return Math.floor(carouselWidth / itemWidth);
1330        }
1331        
1332        // Calculate total number of "pages" (groups of visible items)
1333        function getTotalPages() {
1334          var visibleCount = getVisibleCount();
1335          return Math.ceil($alumniItems.length / visibleCount);
1336        }
1337        
1338        // Create dots container if it doesn't exist
1339        var $dotsContainer = $container.find('.alumni-carousel-dots');
1340        if ($dotsContainer.length === 0) {
1341          $dotsContainer = $('<ul class="alumni-carousel-dots"></ul>');
1342          $container.append($dotsContainer);
1343        }
1344        
1345        // Update dots based on current scroll position
1346        function updateDots() {
1347          var scrollLeft = $carousel[0].scrollLeft;
1348          var carouselWidth = $carousel[0].clientWidth;
1349          var itemWidth = $alumniItems.first().outerWidth(true);
1350          var visibleCount = Math.floor(carouselWidth / itemWidth);
1351          var currentPage = Math.round(scrollLeft / (itemWidth * visibleCount));
1352          var totalPages = Math.ceil($alumniItems.length / visibleCount);
1353          
1354          // Clear and recreate dots
1355          $dotsContainer.empty();
1356          
1357          for (var i = 0; i < totalPages; i++) {
1358            var $dot = $('<li><button type="button" class="alumni-carousel-dot" data-page="' + i + '" aria-label="Страница ' + (i + 1) + '"></button></li>');
1359            if (i === currentPage) {
1360              $dot.find('button').addClass('active');
1361            }
1362            $dotsContainer.append($dot);
1363          }
1364        }
1365        
1366        // Scroll to specific page
1367        function scrollToPage(page) {
1368          var carouselWidth = $carousel[0].clientWidth;
1369          var itemWidth = $alumniItems.first().outerWidth(true);
1370          var visibleCount = Math.floor(carouselWidth / itemWidth);
1371          var scrollPosition = page * itemWidth * visibleCount;
1372          
1373          $carousel.animate({
1374            scrollLeft: scrollPosition
1375          }, 300);
1376        }
1377        
1378        // Click handler for dots
1379        $dotsContainer.on('click', '.alumni-carousel-dot', function() {
1380          var page = parseInt($(this).attr('data-page'));
1381          scrollToPage(page);
1382        });
1383        
1384        // Update dots on scroll
1385        $carousel.on('scroll', updateDots);
1386        
1387        // Update dots on resize
1388        $(window).on('resize', function() {
1389          updateDots();
1390        });
1391        
1392        // Initial dots creation
1393        updateDots();
1394      });
1395      // END: Alumni carousel navigation dots
1396      ////////////////////////////////////////////////////////////////////////
1397
1398      ////////////////////////////////////////////////////////////////////////
1399      // Testimonials carousel on main page
1400      ////////////////////////////////////////////////////////////////////////
1401      $(context).find('.testimonials-main-section .testimonials-carousel').once('testimonialsCarousel').each(function () {
1402        var $carousel = $(this);
1403        // Элементы карусели: сначала ищем .testimonial-item, если их нет — берем .views-row
1404        var $items = $carousel.find('.testimonial-item');
1405        if ($items.length === 0) {
1406          $items = $carousel.find('.views-row');
1407        }
1408        var totalTestimonials = $items.length;
1409        if (!totalTestimonials) {
1410          return;
1411        }
1412
1413        var currentTestimonial = 0;
1414        var autoplayInterval = 20000; // 15 секунд между слайдами
1415        var autoplayTimer = null;
1416
1417        // Контейнер с точками рядом с текущей каруселью
1418        var $section = $carousel.closest('.testimonials-main-section');
1419        var $dotsContainer = $section.find('.testimonials-dots');
1420        $dotsContainer.empty();
1421
1422        for (var i = 0; i < totalTestimonials; i++) {
1423          var $dot = $('<button type="button" class="testimonials-dot"></button>');
1424          $dot.attr('data-index', i);
1425          if (i === 0) {
1426            $dot.addClass('active');
1427          }
1428          $dotsContainer.append($dot);
1429        }
1430
1431        function showTestimonial(index) {
1432          $items.hide();
1433          $items.eq(index).fadeIn(300);
1434
1435          $dotsContainer.find('.testimonials-dot').removeClass('active');
1436          $dotsContainer.find('.testimonials-dot').eq(index).addClass('active');
1437
1438          currentTestimonial = index;
1439        }
1440
1441        function startAutoplay() {
1442          if (autoplayTimer) {
1443            clearInterval(autoplayTimer);
1444          }
1445          autoplayTimer = setInterval(function () {
1446            var nextIndex = (currentTestimonial + 1) % totalTestimonials;
1447            showTestimonial(nextIndex);
1448          }, autoplayInterval);
1449        }
1450
1451        // Показ первого отзыва и запуск автопрокрутки
1452        showTestimonial(0);
1453        startAutoplay();
1454
1455        // Навигация стрелками для текущей секции
1456        $section.find('.testimonials-nav-arrow.prev').on('click', function () {
1457          var prevIndex = (currentTestimonial - 1 + totalTestimonials) % totalTestimonials;
1458          showTestimonial(prevIndex);
1459          startAutoplay();
1460        });
1461
1462        $section.find('.testimonials-nav-arrow.next').on('click', function () {
1463          var nextIndex = (currentTestimonial + 1) % totalTestimonials;
1464          showTestimonial(nextIndex);
1465          startAutoplay();
1466        });
1467
1468        // Навигация точками
1469        $dotsContainer.on('click', '.testimonials-dot', function () {
1470          var index = parseInt($(this).attr('data-index'), 10);
1471          if (!isNaN(index)) {
1472            showTestimonial(index);
1473            startAutoplay();
1474          }
1475        });
1476      });
1477      // END: Testimonials carousel
1478      ////////////////////////////////////////////////////////////////////////
1479
1480      // Страница «О Европейском»: футер после блоков людей, аккордеоны только на телефонах.
1481      $('body.path-european_univers, body.path-en-european_univers', context).once('eu-univers-layout').each(function () {
1482        var $footer = $('#unit-footer');
1483        var $peopleBlocks = $(
1484          '.block-views-blockpeople-block-1, ' +
1485          '.block-views-blockpeople-block-2, ' +
1486          '.block-views-blockpeople-block-3, ' +
1487          '.block-views-blockpeople-block-4'
1488        );
1489        if ($footer.length && $peopleBlocks.length) {
1490          $footer.insertAfter($peopleBlocks.last());
1491        }
1492      });
1493
1494      var euPeopleBlockSelector =
1495        'body.path-european_univers .block-views-blockpeople-block-1, ' +
1496        'body.path-european_univers .block-views-blockpeople-block-2, ' +
1497        'body.path-european_univers .block-views-blockpeople-block-3, ' +
1498        'body.path-european_univers .block-views-blockpeople-block-4, ' +
1499        'body.path-en-european_univers .block-views-blockpeople-block-1, ' +
1500        'body.path-en-european_univers .block-views-blockpeople-block-2, ' +
1501        'body.path-en-european_univers .block-views-blockpeople-block-3, ' +
1502        'body.path-en-european_univers .block-views-blockpeople-block-4';
1503
1504      function isEuUniversMobileAccordion() {
1505        return window.matchMedia('(max-width: 767px)').matches;
1506      }
1507
1508      function teardownEuPeopleAccordion($block) {
1509        if (!$block.hasClass('eu-people-section--ready')) {
1510          return;
1511        }
1512
1513        var $panel = $block.find('.eu-people-accordion-panel').first();
1514        var $teaser = $panel.children('.people-teaser').first();
1515        if ($teaser.length) {
1516          $panel.before($teaser);
1517          $panel.remove();
1518        }
1519
1520        $block.find('.eu-people-block-header').remove();
1521
1522        var $title = $block.find('.eu-people-block-content h2').first();
1523        if ($title.length) {
1524          $title
1525            .addClass('block-title')
1526            .removeClass('eu-people-accordion-toggle collapsed')
1527            .removeAttr('data-toggle href aria-expanded aria-controls role');
1528          $block.prepend($title);
1529        }
1530
1531        $block.removeClass('eu-people-section eu-people-section--ready eu-people-section--accordion');
1532      }
1533
1534      function setupEuPeopleAccordion($block, index) {
1535        if ($block.hasClass('eu-people-section--ready')) {
1536          return;
1537        }
1538
1539        var $title = $block.children('h2.block-title').first();
1540        var $peopleTeaser = $block.find('.people-teaser').first();
1541        if (!$title.length || !$peopleTeaser.length) {
1542          return;
1543        }
1544
1545        var collapseId = 'eu-people-section-' + index;
1546        $block.addClass('eu-people-section eu-people-section--ready eu-people-section--accordion');
1547
1548        $title.detach().removeClass('eu-people-accordion-toggle collapsed');
1549
1550        var $header = $('<div class="eu-people-block-header structure-block-header"></div>');
1551        var $contentWrap = $('<div class="eu-people-block-content structure-block-content"></div>');
1552        $contentWrap.append($title);
1553
1554        var $toggle = $('<a class="eu-people-block-toggle structure-block-toggle collapsed"></a>')
1555          .attr({
1556            'data-toggle': 'collapse',
1557            href: '#' + collapseId,
1558            'aria-expanded': 'false',
1559            'aria-controls': collapseId,
1560            role: 'button'
1561          })
1562          .append($('<div class="structure-block-arrow">▼</div>'));
1563
1564        $header.append($contentWrap).append($toggle);
1565
1566        $peopleTeaser.wrap(
1567          '<div class="collapse eu-people-accordion-panel structure-block-details" id="' + collapseId + '"></div>'
1568        );
1569        $header.insertBefore($block.find('.eu-people-accordion-panel').first());
1570      }
1571
1572      function refreshEuPeopleAccordions(accordionContext) {
1573        var $blocks = $(euPeopleBlockSelector, accordionContext);
1574        if (!$blocks.length) {
1575          return;
1576        }
1577
1578        if (isEuUniversMobileAccordion()) {
1579          $blocks.each(function (index) {
1580            teardownEuPeopleAccordion($(this));
1581            setupEuPeopleAccordion($(this), index);
1582          });
1583        }
1584        else {
1585          $blocks.each(function () {
1586            teardownEuPeopleAccordion($(this));
1587          });
1588        }
1589      }
1590
1591      $('body.path-european_univers, body.path-en-european_univers', context).once('eu-people-accordion-init').each(function () {
1592        refreshEuPeopleAccordions(context);
1593      });
1594
1595      $(window, context).once('eu-people-accordion-resize').on('resize.euPeopleAccordion', function () {
1596        refreshEuPeopleAccordions(document);
1597      });
1598
1599    }
1600  }
1601})(jQuery, Drupal);

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.