PageSourceSearch

https://gifilmfestivalsd.org/content/themes/exhibition/assets/js/theme.js?ver=e91c1fa2

js gifilmfestivalsd.org collected 2026-10-02 11:55:09 UTC 40,423 bytes, 1,158 lines download raw bytes

1/* globals Stripe */
2'use strict';
3window.jQuery( document ).ready( function ($) {
4
5    var currentDonationAmount = 0;
6
7    /**
8     * Page/functionality setup.
9     */
10    toggleNavAriaHidden( false );
11    toggleNavTabindexes( window.innerWidth >= 1024 );
12    toggleNavSubmenuAriaExpanded();
13
14    /**
15     * Global keypress listeners.
16     */
17    $(document).on( 'keypress', function(e) {
18        if ( 13 === e.which ) {
19            handleEnterKeypress(e);
20        }
21    } );
22
23    $(window).on( 'resize', function(e) {
24        toggleNavTabindexes( window.innerWidth >= 1024 );
25    });
26
27    $(document).on( 'click', '[role="checkbox"]', function(e) {
28        handleCheckboxRoleToggle(e);
29    } );
30
31    /**
32     * Returns currency symbol.
33     */
34    function getCurrencySymbol() {
35        var defaultSymbol        = '$';
36        var currencySymbolHolder = $( '#currency-symbol' );
37        if ( currencySymbolHolder.length ) {
38            var currencySymbol = currencySymbolHolder.text();
39            return currencySymbol ? currencySymbol : defaultSymbol;
40        }
41        return defaultSymbol;
42    }
43
44    function moneyFormat( value ) {
45        return parseFloat( value ).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
46    }
47
48    /**
49     * When keypressing Enter on elements with role="link", "checkbox", or "button", trigger a click.
50     *
51     * @param {object} e Original keypress event.
52     */
53    function handleEnterKeypress(e) {
54        var $el  = $(e.target);
55        var role = $el.attr( 'role' );
56
57        if ( 'link' === role || 'button' === role || 'checkbox' === role ) {
58            $el.trigger( 'click' );
59        }
60    }
61
62    /**
63     * Mobile menu toggle.
64     */
65    $( '.menu-toggle' ).click( function () {
66        const $this    = $( this );
67        const $body    = $( 'body' );
68        const $siteNav = $this.parent( '#site-navigation' );
69
70        $siteNav.toggleClass( 'menu-open' );
71        $body.toggleClass( 'modal-open' );
72
73        // Next calls must be after toggleClass calls, so that e.g. when menu is modal-open, aria-expanded is true, and vice versa.
74        const menuOpen = $siteNav.is( '.menu-open' );
75
76        $this.attr( 'aria-expanded', menuOpen );
77
78        toggleNavAriaHidden( menuOpen );
79        toggleNavTabindexes( menuOpen );
80    } );
81
82    /**
83     * Toggles ARIA attributes on nav elements.
84     *
85     * @param {boolean} menuOpen True if mobile menu is open/expanded, false otherwise.
86     */
87    function toggleNavAriaHidden( menuOpen ) {
88        const $siteNav = $( document.getElementById( 'site-navigation' ) );
89
90        $siteNav.find( '.main-nav-container' ).attr( 'aria-hidden', ! menuOpen );
91        $siteNav.find( '#menu-toggle__open' ).attr( 'aria-hidden', menuOpen  );
92        $siteNav.find( '#menu-toggle__close' ).attr( 'aria-hidden', ! menuOpen  );
93    }
94
95    /**
96     * Adjusts nav element tabindexes, so they're included or excluded from tab order when appropriate.
97     *
98     * @param {boolean} menuOpen True if mobile menu is open/expanded, false otherwise.
99     */
100    function toggleNavTabindexes( menuOpen ) {
101        const $primaryMenu    = $( document.getElementById( 'primary-menu' ) );
102        const primarySearch   = document.getElementById( 'primary-nav-search' );
103        const auxiliarySearch = document.getElementById( 'auxiliary-nav-search' );
104        const tabindex        = menuOpen ? '0' : '-1';
105
106        $primaryMenu.find( 'li a' ).each( function( i, el ) {
107            el.setAttribute( 'tabindex', tabindex );
108        } );
109
110        primarySearch && primarySearch.setAttribute( 'tabindex', menuOpen ? '-1' : '0' );
111        auxiliarySearch && auxiliarySearch.setAttribute( 'tabindex', tabindex );
112    }
113
114    /**
115     * Ensures parent menu items' `aria-expanded` attrs are toggled correctly as their submenus are opened/closed.
116     */
117    function toggleNavSubmenuAriaExpanded() {
118        $(document).on( 'mouseenter focus', '.menu-item-has-children', function(e) {
119            $(e.currentTarget).find( '> a' ).attr( 'aria-expanded', 'true' );
120        } );
121
122        $(document).on( 'mouseleave blur', '.menu-item-has-children', function(e) {
123            $(e.currentTarget).find( '> a' ).attr( 'aria-expanded', 'false' );
124        } );
125    }
126
127    /**
128     * Toggles aria-checked attr on items using "checkbox" role.
129     *
130     * @param {object} e Original click event.
131     *
132     * @return {string}
133     */
134    function handleCheckboxRoleToggle(e) {
135        // `prevAttrValue` is always a string or undefined.
136        $(e.target).attr( 'aria-checked', function( i, prevAttrValue ) {
137            if ( 'undefined' === typeof prevAttrValue || 'false' === prevAttrValue ) {
138                return 'true';
139            }
140
141            return 'false';
142        });
143    }
144
145    /**
146     * Updates donation amount.
147     */
148    function setDonationAmount( amount ) {
149        var $donateAmount = $( '.donate-amount' );
150        if ( !$donateAmount.length ) {
151            return;
152        }
153
154        const $purchasePrice = $( '.total_purchase_price' );
155        let orderTotal       = parseFloat( $purchasePrice.first().text().replace( /[^0-9.-]+/g, '' ) );
156
157        // Remove the current donation amount from the total.
158        orderTotal -= currentDonationAmount;
159
160        amount = parseFloat( amount );
161
162        currentDonationAmount = amount;
163        var $donationRow      = $( '.donate' );
164        var isDonationVisible = $donationRow.is( ':visible' );
165
166        // Conditionally toggle the donation row.
167        if ( (amount > 0 && !isDonationVisible) || (!amount && isDonationVisible) ) {
168            $donationRow.stop().slideToggle();
169        }
170
171        $donateAmount.text( moneyFormat(amount) );
172
173        // Add the new donation amount to the total.
174        orderTotal += amount;
175
176        if (orderTotal < 0) {
177            orderTotal = 0;
178        }
179
180        $( '.free-order-payment-info' ).toggle( orderTotal > 0 );
181
182        $purchasePrice.text( getCurrencySymbol() + moneyFormat(orderTotal) );
183
184        updatePaymentMethod( orderTotal );
185        $.themeCheckout.maybeToggleCreditCardFields();
186    }
187
188    $( '[data-target="#trailer-modal"]' ).click( function () {
189        var $trailerUrl = $( this ).data( 'trailer' );
190        $( '#trailer-modal' ).find( '.iframe-holder-inner' ).html( $trailerUrl );
191    } );
192
193    $( document.getElementById( 'trailer-modal' ) ).on( 'hidden.bs.modal', function ( e ) {
194        $( this ).find( '.iframe-holder-inner' ).html( '' );
195    } );
196
197    $( '.show-carousel' ).each( function ( i, e ) {
198        var $this = $( e );
199        var $prev = $this.closest( '.show-slider' ).find( '.show-slider-prev' );
200        var $next = $this.closest( '.show-slider' ).find( '.show-slider-next' );
201
202        $this.slick( {
203            arrows: false,
204            dots: true,
205            infinite: true,
206            slidesToShow: 3,
207            slidesToScroll: 3,
208            variableWidth: false,
209            adaptiveHeight: true,
210            accessibility: false, // Disabling the default accessibility mode prevents Slicks' forceful setting of tabindex=0 on all internal links, which we don't want as it makes navigating with screen-reader more challenging.
211            responsive: [
212                {
213                    breakpoint: 768,
214                    settings: {
215                        slidesToShow: 2,
216                        slidesToScroll: 2
217                    }
218                },
219                {
220                    breakpoint: 480,
221                    settings: {
222                        slidesToShow: 1,
223                        slidesToScroll: 1
224                    }
225                }
226            ]
227        } );
228
229        $prev.on( 'click', function () {
230            $this.slick( 'slickPrev' );
231        } );
232
233        $next.on( 'click', function () {
234            $this.slick( 'slickNext' );
235        } );
236
237        $this.find( '.slick-dots button' ).each(function(i, el) {
238            $(el).attr({
239                'tabindex': '-1',
240                'aria-hidden': true
241            });
242        });
243    } );
244
245    $( '[data-toggle="tooltip"]' ).tooltip();
246
247    $( document.getElementById( 'show-slider' ) ).slick( {
248        arrows: true,
249        prevArrow: '.prev-arrow',
250        nextArrow: '.next-arrow',
251        dots: true,
252        infinite: true,
253        slidesToShow: 1,
254        slidesToScroll: 1
255    } );
256
257    $( '.toggle-ib' ).click( function () {
258        const $tab           = $(this);
259        const $builder       = $tab.closest( '#itinerary-builder' );
260        const $tabindexEls   = $builder.find( '[tabindex]' );
261        const $ariaHiddenEls = $builder.find( '[aria-hidden]' );
262
263        if ( $builder.is( '.ib-open' ) ) {
264
265            $builder.removeClass( 'ib-open' );
266
267            $tabindexEls.each(function(i, el) {
268                $(el).attr( 'tabindex', '-1' );
269            } );
270
271            $ariaHiddenEls.each(function(i, el) {
272                $(el).attr( 'aria-hidden', 'true' );
273            });
274
275            $tab.attr( 'aria-expanded', 'false' );
276
277        } else {
278
279            $builder.addClass( 'ib-open' );
280
281            $tabindexEls.each(function(i, el) {
282                $(el).attr( 'tabindex', '0' );
283            } );
284
285            $ariaHiddenEls.each(function(i, el) {
286                $(el).attr( 'aria-hidden', 'false' );
287            });
288
289            $tab.attr( 'aria-expanded', 'true' );
290        }
291    } );
292
293    maybeInitExtraCarouselItemClasses();
294
295    function maybeInitExtraCarouselItemClasses() {
296        const sliders = document.querySelectorAll( '.hero-slider' );
297
298        if ( 0 === sliders.length ) {
299            return;
300        }
301
302        sliders.forEach( function ( slider ) {
303            const firstSlide = slider.querySelector( '.carousel-item:first-of-type' );
304
305            slider.dataset.carouselItemType = firstSlide.dataset.carouselItemType || '';
306
307            $( slider ).on( 'slide.bs.carousel', function(event) {
308                slider.dataset.carouselItemType = event.relatedTarget.dataset.carouselItemType || '';
309            } );
310        } );
311    }
312
313    maybeInitVideoBg();
314
315    /**
316     * If there are video hero sliders, init their background video players.
317     */
318    function maybeInitVideoBg() {
319        const players = document.querySelectorAll( '[data-vbg]' );
320
321        if ( 0 === players.length ) {
322            return;
323        }
324
325        const videos = new window.VideoBackgrounds( '[data-vbg]' );
326
327        // Ensures both wrapping div and actual iframe are tabindex=-1.
328        if ( 'object' !== typeof videos.index ) {
329            return;
330        }
331
332        Object.keys(videos.index).forEach(function(video) {
333            'object' === typeof videos.index[ video ].element && videos.index[ video ].element.setAttribute( 'tabindex', '-1' );
334            'object' === typeof videos.index[ video ].iframe && videos.index[ video ].iframe.setAttribute( 'tabindex', '-1' );
335
336            const $slider = $( videos.index[ video ].element ).parents( '.hero-slider' );
337            const $play   = $slider.find( '.video-hero__play' );
338            const $pause  = $slider.find( '.video-hero__pause' );
339
340            $play.on( 'click', function(e) {
341                e.preventDefault();
342                document.body.classList.remove( 'filmbot-vbg-paused' );
343                videos.playVideos();
344            } );
345
346            $pause.on( 'click', function(e) {
347                e.preventDefault();
348                document.body.classList.add( 'filmbot-vbg-paused' );
349                videos.pauseVideos();
350            } );
351        } );
352    }
353
354    /**
355     * Share functionality
356     */
357    $( 'a.tweetthis' ).click( function ( e ) {
358        var n = $( this ).attr( 'data-text' );
359
360        window.open( 'http://twitter.com/share?url=' + encodeURIComponent( window.location ) + '&text=' + encodeURIComponent( n ) + '&redirect_uri=' + encodeURIComponent( window.location ), '', 'width=626,height=436' );
361
362        e.preventDefault();
363    } );
364
365    $( 'a.facebookthis' ).click( function ( e ) {
366        var n = $( this ).attr( 'data-text' );
367
368        window.open( 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent( window.location ) + '&display=popup&description=' + encodeURIComponent( n ), '', 'width=626,height=436' );
369        'undefined' != typeof ga && ga && ga( 'send', 'event', 'Keyfindings', 'click', 'facebook button', 1 );
370        e.preventDefault();
371    } );
372
373    /**
374     * Checkout
375     */
376    var $error             = $( '.cart-error' );
377    var $cart              = $( '.cart' );
378    var $discountRow       = $( '.ticket-total-discount-row' );
379    var $discountCode      = $( '.discount-code' );
380    var $discountCodeBtn   = $discountCode.find( 'button' );
381    var $discountCodeInput = $discountCode.find( 'input' );
382
383    /**
384     * Handle ticket type qty change
385     */
386    $( document ).on( 'change', '.ticket-quantity select', function ( e ) {
387        e.preventDefault();
388
389        // Clean up discount code
390        if ( !$discountCodeBtn.hasClass( 'applied' ) ) {
391            $discountCodeInput.val( '' );
392        }
393
394        cartUpdateRequest( getOrderData(), true );
395    } );
396
397    /**
398     * Handle remove ticket type
399     */
400    $( document ).on( 'click', '.remove-ticket_type-btn', function ( e ) {
401        e.preventDefault();
402
403        // Do not allow removing tickets if checkout has already been submitted
404        if ( $.themeCheckout.checkoutSubmitted ) {
405            return;
406        }
407
408        var $order       = getOrderData();
409        var ticket_type  = $( this ).closest( '.ticket-type' ).attr( 'data-ticket_type' );
410        var $showtime_id = $( this ).closest( '.film' ).attr( 'data-showtime' );
411
412        delete $order[ $showtime_id ][ ticket_type ];
413
414        // Clean up discount code
415        if ( ! $discountCodeBtn.hasClass( 'applied' ) ) {
416            $discountCodeInput.val( '' );
417        }
418
419        $( this ).closest( '.ticket-type' ).addClass( 'remove-ticket' );
420
421        cartUpdateRequest( $order, true );
422    } );
423
424    /**
425     * Setup aria-attribute adjustments on Bootstrap dropdown elements.
426     */
427    $( '.dropdown' ).each( function( i, dropdown ) {
428        const $dropdown = $(dropdown);
429        const $menu     = $dropdown.find( '.dropdown-menu' );
430
431        $menu.attr( 'aria-hidden', 'true' );
432
433        $dropdown.on( 'shown.bs.dropdown', function() {
434            $menu.attr( 'aria-hidden', 'false' );
435        } );
436
437        $dropdown.on( 'hidden.bs.dropdown', function() {
438            $menu.attr( 'aria-hidden', 'true' );
439        } );
440    });
441
442    /**
443     * Handle buy a ticket button
444     */
445    $( document ).on( 'click', '.add-to-cart', function ( e ) {
446        e.preventDefault();
447
448        var showtimeId = $( this ).attr( 'data-showtime-id' );
449        var $order     = {};
450        var $this      = $( this );
451        var $dropdown  = $this.closest( '.dropdown' );
452
453        $order[showtimeId] = {};
454
455        $.each( $( '#form-buy-' + showtimeId ).find( 'select' ), function ( i, element ) {
456            var $input = $( element );
457            var ttSlug = $input.data( 'ttslug' );
458            var qty    = $input.val();
459
460            $order[showtimeId][ttSlug] = qty;
461
462            $input.val( '0' );
463        } );
464
465        cartUpdateRequest( $order );
466
467        $dropdown.attr( 'aria-expanded', 'false' ).find( '.dropdown-menu' ).removeClass( 'show' );
468    } );
469
470    $( '.dropdown-menu option, .dropdown-menu select' ).click( function ( e ) {
471        e.stopPropagation();
472    } );
473
474    /**
475     * Handle Apply discount
476     */
477    $( document ).on( 'click', '.discount-code button', function ( e ) {
478        e.preventDefault();
479
480        // Remove discount code
481        if ( $discountCodeBtn.hasClass( 'applied' ) ) {
482            $discountCodeInput.val( '' );
483        }
484
485        cartUpdateRequest( getOrderData(), true );
486    } );
487
488    /**
489     * Handle Apply discount
490     */
491    $( document ).on( 'filmbot-change-discount-option', function ( e ) {
492        e.preventDefault();
493
494        cartUpdateRequest( getOrderData(), true );
495    } );
496
497    /**
498     * Handle toggling the addition of a donation.
499     *
500     * @param {object} e Originating click event.
501     */
502    $( document ).on( 'click', '#donate-me', function ( e ) {
503
504        // Do not allow changing donation amount if checkout has already been submitted
505        if ( $.themeCheckout.checkoutSubmitted ) {
506            return;
507        }
508
509        const $checkbox      = $( '#donate-me' );
510        const donationTotal = $checkbox.is( ':checked' ) ? parseFloat( $checkbox.val() ) : 0;
511        setDonationAmount( donationTotal );
512    } );
513
514    // include donation in checkout (freeform)
515    $( document ).on( 'click', '.donation-variant', function (event) {
516
517        // Do not allow changing donation amount if checkout has already been submitted
518        if ( $.themeCheckout.checkoutSubmitted ) {
519            return;
520        }
521
522        var $this             = $( event.target );
523        var $donationVariants = $( '.donation-variant' );
524        var isAlreadySelected = $this.hasClass( 'selected' );
525        var isCustomVariant   = $this.hasClass( 'custom-donation-variant' );
526        var isTextInput       = $this.hasClass( 'custom-donation-amount' );
527        var donationAmount    = parseFloat( $this.data( 'donation-amount' ) );
528
529        // We can click on input and this can be counted as click on parent element, so we need to check if it's input
530        if ( isTextInput || isCustomVariant ) {
531            var $customVariantInput = isTextInput ? $this : $this.find( '.custom-donation-amount' );
532            var $customVariantWrapper = isTextInput ? $this.parent() : $this;
533            // Set the donation amount to the value of the donation input.
534            donationAmount = $customVariantInput.val() ? parseFloat( $customVariantInput.val() ) : 0;
535            $donationVariants.removeClass( 'selected' );
536            // We don't need to remove class 'selected', as custom variant can be clicked many times to change amount
537            $customVariantWrapper.addClass( 'selected' );
538            setDonationAmount( donationAmount );
539            $customVariantInput.focus();
540        } else {
541            $donationVariants.removeClass( 'selected' );
542            // If standard variant is clicked, and it's already selected, we need to deselect it...
543            isAlreadySelected ? $this.removeClass( 'selected' ) : $this.addClass( 'selected' );
544            // ...and set the donation amount to 0 instead of the value of the donation variant.
545            setDonationAmount( isAlreadySelected ? 0 : donationAmount );
546        }
547    } );
548
549    $( document ).on( 'input', '.custom-donation-amount', function () {
550        var $customDonationAmt = $( '.custom-donation-amount' );
551        var donationAmount = $customDonationAmt.val().replace( /[^0-9.]/g, '' );
552        // Check how many . are in the input. If last symbol is . but we already have one, remove all other dots
553        var dots = donationAmount.match( /\./g );
554        if ( dots && dots.length > 1 && donationAmount.charAt( donationAmount.length - 1 ) === '.' ) {
555            donationAmount = donationAmount.substring( 0, donationAmount.length - 1 );
556        }
557        // if first symbol is 0 but value is longer than 1 symbol - remove this 0 at the start
558        if ( donationAmount.charAt( 0 ) === '0' && donationAmount.length > 1 ) {
559            donationAmount = donationAmount.substring( 1 );
560        }
561        // if we have dot, cut 6 symbols instead of 5
562        donationAmount = ( dots && dots.length > 0 ) ? donationAmount.substring( 0, 6 ) : donationAmount.substring( 0, 5 );
563        // if we have more than 2 symbols after dot, cut it to 2 symbols after dot
564        if ( dots && dots.length > 0 && donationAmount.indexOf( '.' ) !== -1 ) {
565            var parts = donationAmount.split( '.' );
566            if ( parts[1].length > 2 ) {
567                donationAmount = parts[0] + '.' + parts[1].substring( 0, 2 );
568            }
569        }
570        donationAmount = donationAmount && donationAmount !== '.' ? donationAmount : 0;
571        $customDonationAmt.val( donationAmount );
572        setDonationAmount( donationAmount );
573    } );
574
575    /**
576     * Handle payment method select
577     */
578    $( document ).on( 'change', '#pmt-method', function ( e ) {
579        // Toggle credit card fields
580        if ( $( this ).val() == 'cc' ) {
581            showCreditCardFields();
582        } else if ( $( this ).val() == 'cash' ) {
583            hideCreditCardFields();
584        }
585    } );
586
587    /**
588     * Handle comp select
589     */
590    $( document ).on( 'change', '#is-comp', function ( e ) {
591        // Toggle credit card fields
592        if ( $( this ).prop( 'checked' ) === false ) {
593            showCreditCardFields();
594            $( document.getElementById( 'pmt-method' ) ).val( 'cc' );
595            $( '.total_purchase_price' ).text( $( '.total_purchase_price' ).data( 'full-price' ) );
596        } else {
597            hideCreditCardFields();
598            $( document.getElementById( 'pmt-method' ) ).val( 'cash' );
599            $( '.total_purchase_price' ).text( getCurrencySymbol() + '0.00' );
600        }
601    } );
602
603    /**
604     * Update cart totals
605     * @param order
606     */
607    function updateCart( order ) {
608        /**
609         * Update totals
610         */
611        var currencySymbol = getCurrencySymbol();
612
613        $( '.ticket-totals .data-by-ticket-type' ).html( order.html_description );
614
615        $( '.booking_fee_total' ).text( currencySymbol + moneyFormat(order.booking_fee) );
616
617        $( '.total_purchase_price' ).text( currencySymbol + moneyFormat(order.total) );
618
619        if ( order.discount_total ) {
620            $( '.discount_total' ).text( '-' + currencySymbol + moneyFormat(order.discount_total) );
621        }
622
623        /**
624         * Update discount input
625         */
626        toggleApplyDiscount( order.discount_total );
627
628        /**
629         * Update cart items
630         */
631        var ticketsToRemove = $( '.remove-ticket' );
632
633        if ( ticketsToRemove.closest( '.film' ).find( '.ticket-type' ).length === 1 ) {
634            ticketsToRemove.closest( '.film' ).remove();
635        } else {
636            $( ticketsToRemove ).remove();
637        }
638
639        /**
640         * If it's a free order, or a cash order, hide the payment field
641         */
642        if ( (order.total == 0 && $( '.cart-row.film' ).length) ) {
643            hideCreditCardFields();
644        } else if ( $( document.getElementById( 'pmt-method' ) ).val() != 'cash' ) {
645            showCreditCardFields();
646        }
647
648        /**
649         * Update payment method
650         */
651        updatePaymentMethod( order.total );
652        $.themeCheckout.maybeToggleCreditCardFields();
653
654        if ( order.total == 0 ) {
655            $( '.postal-code' ).stop( true ).hide().find( '#postal-code' ).prop( 'disabled', true );
656        }
657    }
658
659    function updatePaymentMethod( orderTotal ) {
660        if ( orderTotal > 0 ) {
661            $( document.getElementById( 'pmt-method' ) ).val( 'cc' ).trigger( 'change' );
662        } else {
663            $( document.getElementById( 'pmt-method' ) ).val( 'cash' ).trigger( 'change' );
664        }
665    }
666
667    function showCreditCardFields() {
668        $( '.payment-info' ).slideDown();
669        $( '.postal-code' ).slideUp().find( '#postal-code' ).prop( 'disabled', true );
670    }
671
672    function hideCreditCardFields() {
673        $( '.payment-info' ).slideUp();
674        $( '.postal-code' ).slideDown().find( '#postal-code' ).prop( 'disabled', false );
675    }
676
677    /**
678     * Toggle discount code state.
679     *
680     * @param discount_total
681     */
682    function toggleApplyDiscount( discount_total ) {
683        if ( discount_total ) {
684            $discountCodeBtn.addClass( 'applied' );
685            $discountCodeBtn.text( 'Remove' );
686            $discountCodeInput.prop( 'readonly', true );
687            $discountRow.show();
688            return;
689        }
690
691        $discountCodeBtn.removeClass( 'applied' );
692        $discountCodeBtn.text( 'Apply' );
693        $discountCodeInput.prop( 'readonly', false );
694        $discountRow.hide();
695    }
696
697    /**
698     * Update floating cart (qty and subtotal)
699     * @param order
700     */
701    function updateFloatingCart( order ) {
702        $( '.floating-cart .quantity' ).text( order.qty );
703        $( '.floating-cart .amount .price' ).text( getCurrencySymbol() + moneyFormat(order.total) );
704
705        if ( parseInt( order.qty, 10 ) === 0 ) {
706            $( '.floating-cart' ).addClass( 'empty' );
707            $( '.floating-cart' ).find( 'a' ).attr( 'tabindex', '-1' );
708        } else {
709            $( '.floating-cart' ).removeClass( 'empty' );
710            $( '.floating-cart' ).find( 'a' ).attr( 'tabindex', '0' );
711        }
712    }
713
714    /**
715     * Get order data
716     */
717    function getOrderData() {
718        var $order = {};
719        var ticketType;
720
721        $( '.film' ).each( function ( i, showtime ) {
722            var showtime_id = $( this ).attr( 'data-showtime' );
723
724            $( showtime ).find( '.ticket-type' ).each( function ( i, ticket ) {
725                ticketType = $( ticket ).attr( 'data-ticket_type' );
726
727                if ( !$order[ showtime_id ] ) {
728                    $order[ showtime_id ] = {};
729                }
730
731                if ( !$order[ showtime_id ][ ticketType ] ) {
732                    $order[ showtime_id ][ ticketType ] = 0;
733                }
734
735                $order[ showtime_id ][ ticketType ] += parseInt( $( ticket ).find( '.ticket-quantity select' ).val() );
736            } );
737        } );
738
739        return $order;
740    }
741
742    /**
743     * The donation amount total.
744     *
745     * @returns {Number}
746     */
747    function getDonationTotal() {
748        var $donateAmountField = $( '.donate-amount' );
749        return $donateAmountField.length ? parseFloat( $donateAmountField.first().text().replace( /[^0-9.-]+/g, '' ) ) : 0;
750    }
751
752    /**
753     * Make a request to Cart_Handler
754     * @param $order
755     * @param isUpdate
756     */
757    function cartUpdateRequest( $order, isUpdate ) {
758        $cart.css( 'pointer-events', 'none' );
759        $error.hide();
760
761        var $email = $( document.getElementById( 'email' ) );
762        var email  = $email.length ? $email.val() : '';
763
764        var data = {
765            action: 'exhibition_cart_handler',
766            _ajax_nonce: $( document.getElementById( 'filmbot_checkout_nonce' ) ).val(),
767            promotion_code: $( '.discount-code input' ).val(),
768            email: email,
769            type: isUpdate ? 'update-cart' : 'add-to-cart',
770            order: $order
771        };
772
773        if ( typeof $.applyFilter !== 'undefined' ) {
774            data = $.applyFilter( 'filmbot-modify-discount-data', data );
775        }
776
777        $.ajax( {
778            url: themeSettings.ajaxUrl,
779            method: 'POST',
780            timeout: 60000,
781            data: data,
782            success: function ( response ) {
783                if ( response.success ) {
784                    updateFloatingCart( response.data );
785
786                    if ( isUpdate ) {
787                        response.data.total += getDonationTotal();
788                        updateCart( response.data );
789                    }
790
791                    if( data.promotion_code ) {
792                        jQuery( document ).trigger( 'filmbot-discount-code-applied', response );
793                    } else {
794                        jQuery( document ).trigger( 'filmbot-discount-code-removed' );
795                    }
796                }
797
798                var errors = '';
799
800                if ( ! response.success ) {
801                    errors += response.data;
802                }
803
804                if ( typeof response.promotion_error != 'undefined' ) {
805                    errors += response.promotion_error;
806                    $discountCodeInput.val( '' );
807                    toggleApplyDiscount( 0 );
808                }
809
810                if ( errors.length ) {
811                    $error.text( errors ).show();
812                }
813
814                $cart.css( 'pointer-events', 'all' );
815            },
816            error: function ( xhr, status, error ) {
817                $error.text( error ).show();
818                $cart.css( 'pointer-events', 'all' );
819            }
820        } );
821    }
822
823    /**
824     * Schedule page
825     */
826    $( '.schedule-filters select' ).change( function () {
827        var date           = $( document.getElementById( 'filter-date' ) ).val();
828        var postId         = $( document.getElementById( 'filter-post-id' ) ).val();
829        var section        = $( document.getElementById( 'filter-section' ) ).val();
830        var venue          = $( document.getElementById( 'filter-venue' ) ).val();
831        var showtimes      = $( '.event-row' );
832        var groupShowtimes = {};
833        var show           = '';
834
835        $( '.data-block' ).hide();
836
837        showtimes.hide();
838
839        $.each( showtimes, function ( index, element ) {
840            show = $( element ).parent( ' .data-block ' ).attr( 'data-show' );
841
842            if ( ! groupShowtimes[ show ] ) {
843                groupShowtimes[ show ] = [];
844            }
845
846            groupShowtimes[ show ].push( element );
847        } );
848
849        var filteredData = {};
850
851        $.each( groupShowtimes, function ( key, array ) {
852
853            if ( (date !== 'all') && date !== key ) {
854                return true;
855            }
856
857            var filteredDateShowtimes = [];
858
859            $.each( array, function ( index, element ) {
860
861                if ( ((postId !== 'all') && postId !== $( element ).attr( 'data-
861post-id' )) ) {
862                    return true;
863                }
864
865                if ( (section !== 'all' && typeof section != 'undefined') && !$( element ).attr( 'data-section' ).includes( section ) ) {
866                    return true;
867                }
868                if ( (venue !== 'all') && !$( element ).attr( 'data-venue' ).includes( venue ) ) {
869                    return true;
870                }
871
872                filteredDateShowtimes.push( element );
873            } );
874
875            if ( filteredDateShowtimes.length ) {
876                filteredData[ key ] = filteredDateShowtimes;
877            }
878        } );
879
880        $.each( filteredData, function ( key, array ) {
881            $( '*[data-show="' + key + '"]' ).show();
882            $.each( array, function ( index, element ) {
883                $( element ).show();
884            } );
885        } );
886    } );
887
888    if ( $( document.getElementById( 'itinerary-builder' ) ).length ) {
889        if ( localStorage.getItem( 'showtimes' ) ) {
890            var showtimes = JSON.parse( localStorage.getItem( 'showtimes' ) );
891
892            for ( var i in showtimes ) {
893                var showtimeId = showtimes[i];
894
895                $( 'a[data-showtime=' + showtimeId + ']' ).addClass( 'saved' );
896            }
897
898            updateItineraryBuilder();
899
900            if ( $( '.add-to-itinerary.saved' ).length ) {
901                $( document.getElementById( 'itinerary-builder' ) ).addClass( 'ib-open' );
902            }
903        }
904    }
905
906    function updateItineraryBuilder() {
907        var showtimes      = [];
908        var href           = window.location.href.split( '?' )[0];
909        var itineraryUrl   = href;
910        var itineraryTitle = $( '#itinerary-title' ).val();
911
912        $( 'a.add-to-itinerary.saved' ).each( function ( i, e ) {
913            var showtimeId = $( this ).attr( 'data-showtime' );
914
915            if ( !showtimes.includes( showtimeId ) ) {
916                showtimes[showtimes.length] = showtimeId;
917            } else {
918                var index = showtimes.indexOf( showtimeId );
919                if ( index > -1 ) {
920                    showtimes.splice( index, 1 );
921                }
922            }
923        } );
924
925        if ( showtimes.length == 0 ) {
926            clearItineraryBuilder();
927            return;
928        }
929
930        localStorage.setItem( 'showtimes', JSON.stringify( showtimes ) );
931
932        $( document.getElementById( 'count-itinerary' ) ).text( showtimes.length );
933
934        itineraryUrl = itineraryUrl + '?si=' + showtimes.join( ',' );
935
936        if ( itineraryTitle ) {
937            itineraryUrl = itineraryUrl + '&t=' + itineraryTitle;
938        }
939
940        $( document.getElementById( 'itinerary-url' ) ).val( itineraryUrl );
941        $( document.getElementById( 'open-itinerary' ) ).attr( 'href', itineraryUrl );
942    }
943
944    $( '.add-to-itinerary ' ).click( function ( e ) {
945        e.preventDefault();
946
947        var $this = $( this );
948
949        if ( $this.hasClass( 'saved' ) ) {
950            $this.removeClass( 'saved' );
951            updateItineraryBuilder();
952            return;
953        }
954
955        var conflicts = checkItineraryConflicts( $this );
956
957        if ( ! conflicts.length ) {
958            $this.addClass( 'saved' );
959        } else {
960            var confirmMsg = 'This showtime overlaps with the following:<ul><li>' + conflicts.join( '</li><li>' ) + '</li></ul>';
961
962            bootbox.confirm( {
963                title: 'Schedule conflict',
964                message: confirmMsg,
965                buttons: {
966                    confirm: { label: 'Add anyway' },
967                    cancel: { label: 'Cancel' }
968                },
969                callback: function ( confirmed ) {
970                    if ( confirmed === true ) {
971                        $this.addClass( 'saved' );
972                        updateItineraryBuilder();
973                    }
974                }
975            } );
976        }
977
978        updateItineraryBuilder();
979
980        if ( $( '.add-to-itinerary.saved' ).length == 1 ) {
981            $( document.getElementById( 'itinerary-builder' ) ).addClass( 'ib-open' );
982        }
983    } );
984
985    $( document.getElementById( 'itinerary-title' ) ).change( function () {
986        var itineraryUrl   = $( document.getElementById( 'itinerary-url' ) ).val();
987        var itineraryTitle = $( this ).val();
988
989        if ( itineraryUrl ) {
990            itineraryUrl = itineraryUrl.split( '&t=' )[0] + '&t=' + itineraryTitle;
991
992            $( document.getElementById( 'itinerary-url' ) ).val( itineraryUrl );
993            $( document.getElementById( 'open-itinerary' ) ).attr( 'href', itineraryUrl );
994        }
995    } );
996
997    function clearItineraryBuilder() {
998        localStorage.removeItem( 'showtimes' );
999
1000        $( document.getElementById( 'itinerary-url' ) ).val( '' );
1001        $( document.getElementById( 'open-itinerary' ) ).attr( 'href', '' );
1002        $( document.getElementById( 'itinerary-title' ) ).val( '' );
1003        $( document.getElementById( 'count-itinerary' ) ).text( '0' );
1004        $( 'a.add-to-itinerary' ).removeClass( 'saved' );
1005    }
1006
1007    $( '.start-over' ).click( function ( e ) {
1008        if ( $( this ).attr( 'href' ) == '#' ) {
1009            e.preventDefault();
1010        }
1011
1012        clearItineraryBuilder();
1013    } );
1014
1015    $( document.getElementById( 'download-ics' ) ).click( function ( e ) {
1016        e.preventDefault();
1017
1018        var url = $( document.getElementById( 'itinerary-url' ) ).val();
1019
1020        if ( url ) {
1021            window.open( url + '&ics=1', '_self' );
1022        }
1023    } );
1024
1025    function checkItineraryConflicts( $newShowtime ) {
1026
1027        if ( ! localStorage.getItem( 'showtimes' ) ) {
1028            return [];
1029        }
1030
1031        var newShowtimeInfo = $newShowtime.data( 'showtimeInfo' );
1032        var conflicts       = [];
1033        var timeBetween     = 900; // 15 mins
1034        var showtimes       = JSON.parse( localStorage.getItem( 'showtimes' ) );
1035
1036        // check if it'll conflict with any other schedule items
1037        // 2 conditions will add a conflict:
1038        // 1) new item starts less than 15 mins after other item ends
1039        // 2) new item ends less than 15 mins before other item starts
1040        $.each( showtimes, function ( index, showtimeId ) {
1041            var existingShowtimeInfo = $( 'a[data-showtime=' + showtimeId + ']' ).data( 'showtimeInfo' );
1042
1043            // if a schedule item doesn't have an end time, don't compare with it
1044            if ( ! ( 'datetime_end' in existingShowtimeInfo ) || ! ( 'datetime_end' in newShowtimeInfo ) ) {
1045                return;
1046            }
1047
1048            if (
1049              ( newShowtimeInfo.datetime_start <= existingShowtimeInfo.datetime_end + timeBetween ) &&
1050              ( newShowtimeInfo.datetime_end + timeBetween >= existingShowtimeInfo.datetime_start )
1051            ) {
1052                conflicts.push( (existingShowtimeInfo.title) + ' (' + existingShowtimeInfo.venue + ')' );
1053            }
1054        } );
1055
1056        return conflicts;
1057    }
1058
1059    $( '.page-template-schedule a.tweetthis' ).click( function ( e ) {
1060        var text = '';
1061        var url  = $( document.getElementById( 'itinerary-url' ) ).val();
1062
1063        window.open( 'http://twitter.com/share?url=' + encodeURIComponent( url ) + '&text=' + encodeURIComponent( text ) + '&redirect_uri=' + encodeURIComponent( url ), '', 'width=626,height=436' );
1064
1065        e.preventDefault();
1066    } );
1067
1068    $( '.page-template-schedule a.facebookthis' ).click( function ( e ) {
1069        var n = '';
1070        var t = $( document.getElementById( 'itinerary-url' ) ).val();
1071
1072        window.open( 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent( t ) + '&display=popup&description=' + encodeURIComponent( n ) + '&redirect_uri=' + encodeURIComponent( t ), '', 'width=626,height=436' );
1073
1074        e.preventDefault();
1075    } );
1076
1077    $( '.page-template-schedule a.mail' ).click( function ( e ) {
1078        var t = $( document.getElementById( 'itinerary-url' ) ).val();
1079
1080        window.open( 'mailto:?Schedule Itinerary ' + encodeURIComponent( t ), '_self' );
1081        e.preventDefault();
1082    } );
1083
1084    $( '.page-template-schedule' ).ready( function () {
1085        var regex  = /[?&]([^=#]+)=([^&#]*)/g;
1086        var params = {};
1087        var href   = window.location.href;
1088        var match;
1089
1090        while ( match = regex.exec( href ) ) {
1091            params[match[1]] = match[2];
1092        }
1093
1094        if ( params.show ) {
1095            $( document.getElementById( 'filter-date' ) ).val( params.show );
1096            $( '.page-template-schedule #filter-date' ).change();
1097        }
1098    } );
1099
1100    // Change states on country change
1101    $( document ).on( 'change', '[data-country-selector]', function ( e ) {
1102        updateSelectableCountryStates( e );
1103    } );
1104
1105    /**
1106     * Updates states of selected country in states field.
1107     *
1108     * @param {object} e Original click event.
1109     */
1110    function updateSelectableCountryStates( e ) {
1111        var $statesFieldId         = $( e.target ).data( 'country-selector' );
1112        var $statesField           = $( '#' + $statesFieldId );
1113        var filmbotCountriesStates = window.filmbotCountriesStates || {};
1114
1115        // If there is no states field, or there are no countries and states data, do nothing
1116        if ( !$statesFieldId || !$statesField.length || !Object.keys( filmbotCountriesStates ).length ) {
1117            return;
1118        }
1119
1120        var selectedCountry = $( e.target ).val() || 'US';
1121        var previousCountry = $statesField.data( 'state-selector' );
1122
1123        var statesList     = filmbotCountriesStates[selectedCountry];
1124        var prevStatesList = filmbotCountriesStates[previousCountry];
1125
1126        // Make changes only if selected country is different from previous one
1127        if ( !previousCountry || previousCountry === selectedCountry || !statesList ) {
1128            return;
1129        }
1130
1131        // Check if there are no states for the selected country
1132        var currentHasStates  = !(Array.isArray( statesList.states ) && statesList.states.length === 0);
1133        var previousHasStates = !(Array.isArray( prevStatesList.states ) && prevStatesList.states.length === 0);
1134
1135        // Remove all states of previous country
1136        $statesField.find( 'option[data-state]' ).remove();
1137
1138        // Create new options and append them to the states field
1139        $.each( statesList.states, function ( key, value ) {
1140            $statesField.append( $( '<option data-state value="' + key + '">' + value + '</option>' ) );
1141        } );
1142
1143        // If current country has no states, but previous had, or vice versa,
1144        // update requiredness and placeholders of the states select field
1145        if ( currentHasStates !== previousHasStates ) {
1146
1147            $statesField.toggleClass( 'required', currentHasStates );
1148            currentHasStates ? $statesField.attr( 'required', 'required' ) : $statesField.removeAttr( 'required' );
1149
1150            $statesField.find( 'option[data-empty-state]' ).first().text( currentHasStates ? 'State*' : 'State' );
1151        }
1152
1153        // set the new selected country as data attribute
1154        $statesField.data( 'state-selector', selectedCountry );
1155        $statesField.val( '' );
1156    }
1157
1158} );

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.