Untitled
unknown
plain_text
6 months ago
307 kB
26
Indexable
Never
/* The header.liquid file section */ {%- liquid assign main_menu = linklists[section.settings.main_menu_link_list] assign toolbar_menu = linklists[section.settings.toolbar_menu] assign logo_alignment = 'left' if section.settings.main_menu_alignment == 'center-left' or section.settings.main_menu_alignment == 'center-split' or section.settings.main_menu_alignment == 'center' or section.settings.main_menu_alignment == 'center-drawer' assign logo_alignment = 'center' endif assign dropdown_alignment = 'left' if section.settings.main_menu_alignment == 'left-center' or section.settings.main_menu_alignment == 'center-split' or section.settings.main_menu_alignment == 'center' assign dropdown_alignment = 'center' endif assign template_name = template | replace: '.', ' ' | truncatewords: 2, '' | handle assign sticky_header = false if section.settings.header_style == 'sticky' assign sticky_header = true endif assign overlay_header = false if template_name == 'index' and section.settings.sticky_index assign overlay_header = true endif if template_name contains 'collection' and collection.image and section.settings.sticky_collection assign overlay_header = true endif -%} {% assign main_menu_desktop = linklists[section.settings.main_menu_desktop_link_list] %} {% assign main_menu_mobile = linklists[section.settings.main_menu_mobile_link_list] %} {%- render 'drawer-menu', section: section, main_menu: main_menu_mobile, toolbar_menu: toolbar_menu, logo_alignment: logo_alignment -%} {%- render 'cart-drawer' -%} <style> .site-nav__link, .site-nav__dropdown-link:not(.site-nav__dropdown-link--top-level) { font-size: {{ settings.type_navigation_size }}px; } {% if settings.type_navigation_capitalize %} .site-nav__link, .mobile-nav__link--top-level { text-transform: uppercase; letter-spacing: 0.2em; } .mobile-nav__link--top-level { font-size: 1.1em; } {% endif %} {% if mainmenu.length > 6 %} .site-nav__link { padding-left: 10px; padding-right: 10px; } {% endif %} {% unless section.settings.mega_menu_images %} .megamenu__collection-image { display: none; } {% endunless %} {%- if settings.color_header == settings.color_body_bg or settings.color_body_bg contains settings.color_header -%} .site-header { box-shadow: 0 0 1px rgba(0,0,0,0.2); } .toolbar + .header-sticky-wrapper .site-header { border-top: 0; } {%- endif -%} </style> <div data-section-id="{{ section.id }}" data-section-type="header"> {%- unless overlay_header -%} {%- if section.settings.show_locale_selector or section.settings.show_currency_selector or section.settings.toolbar_social or section.settings.toolbar_menu != blank -%} {%- render 'toolbar', section: section, toolbar_menu: toolbar_menu, overlay_header: overlay_header -%} {%- endif -%} {%- endunless -%} <div class="header-sticky-wrapper"> <div id="HeaderWrapper" class="header-wrapper{% if overlay_header %} header-wrapper--sticky is-light{% endif %}"> {%- if overlay_header -%} {%- if section.settings.show_locale_selector or section.settings.show_currency_selector or section.settings.toolbar_social or section.settings.toolbar_menu != blank -%} {%- render 'toolbar', section: section, toolbar_menu: toolbar_menu, overlay_header: overlay_header -%} {%- endif -%} {%- endif -%} <header id="SiteHeader" class="site-header{% if settings.type_navigation_style == 'heading' %} site-header--heading-style{% endif %}" data-sticky="{{ sticky_header }}" data-overlay="{{ overlay_header }}"> <div class="page-width"> <div class="header-layout header-layout--{{ section.settings.main_menu_alignment }}" data-logo-align="{{ logo_alignment }}"> {%- if logo_alignment == 'left' -%} <div class="header-item header-item--logo"> {%- render 'header-logo-block', section: section -%} </div> {%- endif -%} {%- if logo_alignment == 'left' and section.settings.main_menu_alignment != 'left-drawer' -%} <div class="header-item header-item--navigation{% if section.settings.main_menu_alignment == 'left-center' %} text-center{% endif %}"> {%- render 'header-desktop-nav', main_menu: main_menu, dropdown_alignment: dropdown_alignment -%} </div> {%- endif -%} {%- if logo_alignment == 'center' -%} <div class="header-item header-item--left header-item--navigation"> {%- if section.settings.main_menu_alignment == 'center' or section.settings.main_menu_alignment == 'center-split' -%} {%- if settings.search_enable -%} <div class="site-nav small--hide"> <a href="{{ routes.search_url }}" class="site-nav__link site-nav__link--icon js-search-header"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-search" viewBox="0 0 64 64"><path d="M47.16 28.58A18.58 18.58 0 1 1 28.58 10a18.58 18.58 0 0 1 18.58 18.58zM54 54L41.94 42"/></svg> <span class="icon__fallback-text">{{ 'general.search.title' | t }}</span> </a> </div> {%- endif -%} {%- endif -%} {%- if section.settings.main_menu_alignment == 'center-left' -%} {%- render 'header-desktop-nav', main_menu: main_menu_desktop, dropdown_alignment: dropdown_alignment -%} {%- endif -%} <div class="site-nav{% unless section.settings.main_menu_alignment == 'center-drawer' %} medium-up--hide{% endunless %}"> <button type="button" class="site-nav__link site-nav__link--icon js-drawer-open-nav" aria-controls="NavDrawer"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-hamburger" viewBox="0 0 64 64"><path d="M7 15h51M7 32h43M7 49h51"/></svg> <span class="icon__fallback-text">{{ 'general.drawers.navigation' | t }}</span> </button> </div> </div> {%- if section.settings.main_menu_alignment == 'center-split' -%} {%- render 'header-split-nav', main_menu: main_menu_desktop, section: section, dropdown_alignment: dropdown_alignment -%} {%- endif -%} {%- if section.settings.main_menu_alignment != 'center-split' -%} <div class="header-item header-item--logo"> {%- render 'header-logo-block', section: section -%} </div> {%- endif -%} {%- endif -%} <div class="header-item header-item--icons"> {%- render 'header-icons', section: section -%} </div> </div> {%- if section.settings.main_menu_alignment == 'center' -%} <div class="text-center"> {%- render 'header-desktop-nav', main_menu: main_menu_desktop, dropdown_alignment: dropdown_alignment -%} </div> {%- endif -%} </div> <div class="site-header__search-container"> <div class="site-header__search"> <div class="page-width"> <form action="{{ routes.search_url }}" method="get" role="search" id="HeaderSearchForm" class="site-header__search-form"> <input type="hidden" name="type" value="{{ settings.search_type }}"> <input type="hidden" name="options[prefix]" value="last"> <label for="search-icon" class="hidden-label">{{ 'general.search.submit' | t }}</label> <label for="SearchClose" class="hidden-label">{{ 'general.accessibility.close_modal' | t | json }}</label> <button type="submit" id="search-icon" class="text-link site-header__search-btn site-header__search-btn--submit"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-search" viewBox="0 0 64 64"><path d="M47.16 28.58A18.58 18.58 0 1 1 28.58 10a18.58 18.58 0 0 1 18.58 18.58zM54 54L41.94 42"/></svg> <span class="icon__fallback-text">{{ 'general.search.submit' | t }}</span> </button> <input type="search" name="q" value="{{ search.terms | escape | replace: '*', '' }}" placeholder="{{ 'general.search.placeholder' | t }}" class="site-header__search-input" aria-label="{{ 'general.search.placeholder' | t }}"> </form> <button type="button" id="SearchClose" class="js-search-header-close text-link site-header__search-btn"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-close" viewBox="0 0 64 64"><path d="M19 17.61l27.12 27.13m0-27.12L19 44.74"/></svg> <span class="icon__fallback-text">{{ 'general.accessibility.close_modal' | t | json }}</span> </button> </div> </div> {%- if settings.predictive_search_enabled -%} <div id="PredictiveWrapper" class="predictive-results hide" data-image-size="{{ settings.predictive_image_size }}"> <div class="page-width"> <div id="PredictiveResults" class="predictive-result__layout"></div> <div class="text-center predictive-results__footer"> <button type="button" class="btn btn--small" data-predictive-search-button> <small> {{ 'general.search.view_more' | t }} </small> </button> </div> </div> </div> {%- endif -%} </div> </header> </div> </div> </div> {% schema %} { "name": "t:sections.header.name", "settings": [ { "type": "link_list", "id": "main_menu_desktop_link_list", "label": "Desktop Menu" }, { "type": "link_list", "id": "main_menu_mobile_link_list", "label": "Mobile Menu" }, { "type": "checkbox", "id": "mega_menu_images", "label": "t:sections.header.settings.mega_menu_images.label", "default": true, "info": "t:sections.header.settings.mega_menu_images.info" }, { "type": "select", "id": "main_menu_alignment", "label": "t:sections.header.settings.main_menu_alignment.label", "default": "left", "options": [ { "value": "left", "label": "t:sections.header.settings.main_menu_alignment.options.left.label" }, { "value": "left-center", "label": "t:sections.header.settings.main_menu_alignment.options.left-center.label" }, { "value": "left-drawer", "label": "t:sections.header.settings.main_menu_alignment.options.left-drawer.label" }, { "value": "center-left", "label": "t:sections.header.settings.main_menu_alignment.options.center-left.label" }, { "value": "center-split", "label": "t:sections.header.settings.main_menu_alignment.options.center-split.label" }, { "value": "center", "label": "t:sections.header.settings.main_menu_alignment.options.center.label" }, { "value": "center-drawer", "label": "t:sections.header.settings.main_menu_alignment.options.center-drawer.label" } ] }, { "type": "select", "id": "header_style", "label": "t:sections.header.settings.header_style.label", "default": "normal", "options": [ { "value": "normal", "label": "t:sections.header.settings.header_style.options.normal.label" }, { "value": "sticky", "label": "t:sections.header.settings.header_style.options.sticky.label" } ] }, { "type": "checkbox", "id": "sticky_index", "label": "t:sections.header.settings.sticky_index.label", "default": false }, { "type": "checkbox", "id": "sticky_collection", "label": "t:sections.header.settings.sticky_collection.label", "info": "t:sections.header.settings.sticky_collection.info", "default": false }, { "type": "header", "content": "t:sections.header.settings.header_toolbar" }, { "type": "link_list", "id": "toolbar_menu", "label": "t:sections.header.settings.toolbar_menu.label", "info": "t:sections.header.settings.toolbar_menu.info" }, { "type": "checkbox", "id": "toolbar_social", "label": "t:sections.header.settings.toolbar_social.label" }, { "type": "header", "content": "t:sections.header.settings.header_language_selector", "info": "t:sections.header.settings.header_language_selector" }, { "type": "checkbox", "id": "show_locale_selector", "label": "t:sections.header.settings.show_locale_selector.label", "default": true }, { "type": "header", "content": "t:sections.header.settings.header_currency_selector", "info": "t:sections.header.settings.header_currency_selector" }, { "type": "checkbox", "id": "show_currency_selector", "label": "t:sections.header.settings.show_currency_selector.label", "default": true }, { "type": "checkbox", "id": "show_currency_flags", "label": "t:sections.header.settings.show_currency_flags.label", "default": true } ], "blocks": [ { "type": "logo", "name": "t:sections.header.blocks.logo.name", "limit": 1, "settings": [ { "type": "image_picker", "id": "logo", "label": "t:sections.header.blocks.logo.settings.logo.label" }, { "type": "image_picker", "id": "logo-inverted", "label": "t:sections.header.blocks.logo.settings.logo-inverted.label", "info": "t:sections.header.blocks.logo.settings.logo-inverted.info" }, { "type": "range", "id": "desktop_logo_width", "label": "t:sections.header.blocks.logo.settings.desktop_logo_width.label", "default": 200, "min": 100, "max": 400, "step": 10, "unit": "px" }, { "type": "range", "id": "mobile_logo_width", "label": "t:sections.header.blocks.logo.settings.mobile_logo_width.label", "default": 140, "min": 60, "max": 200, "step": 10, "unit": "px", "info": "t:sections.header.blocks.logo.settings.mobile_logo_width.info" } ] }, { "type": "announcement", "name": "t:sections.header.blocks.announcement.name", "limit": 3, "settings": [ { "type": "text", "id": "text", "label": "t:sections.header.blocks.announcement.settings.text.label", "default": "Hassle-free returns" }, { "type": "text", "id": "link_text", "label": "t:sections.header.blocks.announcement.settings.link_text.label", "default": "30-day postage paid returns" }, { "type": "url", "id": "link", "label": "t:sections.header.blocks.announcement.settings.link.label" } ] } ], "default": { "settings": {} }, "disabled_on": { "groups": ["footer", "custom.popups"] } } {% endschema %} /* The drawer-menu.liquid file snippet */ {%- assign animation_row = 1 -%} {%- assign drawer_position = 'right' -%} {% if logo_alignment == 'center' %} {%- assign drawer_position = 'left' -%} {% endif %} <div id="NavDrawer" class="drawer drawer--{{ drawer_position }}"> <div class="drawer__contents"> <div class="drawer__fixed-header"> <div class="drawer__header appear-animation appear-delay-{{ animation_row }}"> <div class="h2 drawer__title"></div> <div class="drawer__close"> <button type="button" class="drawer__close-button js-drawer-close"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-close" viewBox="0 0 64 64"><path d="M19 17.61l27.12 27.13m0-27.12L19 44.74"/></svg> <span class="icon__fallback-text">{{ 'general.drawers.close_menu' | t }}</span> </button> </div> </div> </div> <div class="drawer__scrollable"> <ul class="mobile-nav{% if settings.type_navigation_style == 'heading' %} mobile-nav--heading-style{% endif %}" role="navigation" aria-label="Primary"> {%- for link in main_menu.links -%} {%- assign animation_row = animation_row | plus: 1 -%} {%- assign child_list_handle = link.url | handleize | append: forloop.index -%} {%- assign has_dropdown = false -%} {%- if link.links != blank -%} {%- assign has_dropdown = true -%} {%- endif -%} <li class="mobile-nav__item appear-animation appear-delay-{{ animation_row }}"> {%- if has_dropdown -%} <div class="mobile-nav__has-sublist"> {%- if link.url == '#' -%} <button type="button" aria-controls="Linklist-{{ child_list_handle }}"{% if link.active or link.child_active %} aria-open="true"{% endif %} class="mobile-nav__link--button mobile-nav__link--top-level collapsible-trigger collapsible--auto-height{% if link.active or link.child_active %} is-open{% endif %}"> <span class="mobile-nav__faux-link"{% if link.active %} data-active="true"{% endif %}> {{ link.title }} </span> <div class="mobile-nav__toggle"> <span class="faux-button"> {%- render 'collapsible-icons' -%} </span> </div> </button> {%- else -%} <a href="{{ link.url }}" class="mobile-nav__link mobile-nav__link--top-level" id="Label-{{ child_list_handle }}" {% if link.active %}data-active="true"{% endif %}> {{ link.title }} </a> <div class="mobile-nav__toggle"> <button type="button" aria-controls="Linklist-{{ child_list_handle }}"{% if link.active or link.child_active %} aria-open="true"{% endif %} aria-labelledby="Label-{{ child_list_handle }}" class="collapsible-trigger collapsible--auto-height{% if link.active or link.child_active %} is-open{% endif %}"> {%- render 'collapsible-icons' -%} </button> </div> {%- endif -%} </div> {%- else -%} <a href="{{ link.url }}" class="mobile-nav__link mobile-nav__link--top-level"{% if link.active %} data-active="true"{% endif %}>{{ link.title }}</a> {%- endif -%} {%- if has_dropdown -%} <div id="Linklist-{{ child_list_handle }}" class="mobile-nav__sublist collapsible-content collapsible-content--all{% if link.active or link.child_active %} is-open{% endif %}" {% if link.active or link.child_active %}style="height: auto;"{% endif %}> <div class="collapsible-content__inner"> <ul class="mobile-nav__sublist"> {%- for childlink in link.links -%} {%- assign has_sub_dropdown = false -%} {%- assign grand_child_list_handle = childlink.url | handleize | append: forloop.index -%} {%- if childlink.links != blank -%} {%- assign has_sub_dropdown = true -%} {%- endif -%} <li class="mobile-nav__item"> <div class="mobile-nav__child-item"> {%- if childlink.url == '#' and has_sub_dropdown -%} <button type="button" aria-controls="Sublinklist-{{ child_list_handle }}-{{ grand_child_list_handle }}"{% if childlink.active or childlink.child_active %} aria-open="true"{% endif %} class="mobile-nav__link--button collapsible-trigger{% if childlink.active or childlink.child_active %} is-open{% endif %}"> <span class="mobile-nav__faux-link"{% if childlink.active %} data-active="true"{% endif %}>{{ childlink.title | escape }}</span> {%- render 'collapsible-icons-alt' -%} </button> {%- else -%} <a href="{{ childlink.url }}" class="mobile-nav__link" id="Sublabel-{{ grand_child_list_handle }}" {% if childlink.active %}data-active="true"{% endif %}> {{ childlink.title | escape }} </a> {%- endif -%} {%- if childlink.url != '#' and has_sub_dropdown -%} <button type="button" aria-controls="Sublinklist-{{ child_list_handle }}-{{ grand_child_list_handle }}" aria-labelledby="Sublabel-{{ grand_child_list_handle }}" class="collapsible-trigger{% if childlink.active or childlink.child_active %} is-open{% endif %}"> {%- render 'collapsible-icons-alt' -%} </button> {%- endif -%} </div> {%- if has_sub_dropdown -%} <div id="Sublinklist-{{ child_list_handle }}-{{ grand_child_list_handle }}" aria-labelledby="Sublabel-{{ grand_child_list_handle }}" class="mobile-nav__sublist collapsible-content collapsible-content--all{% if childlink.active or childlink.child_active %} is-open{% endif %}" {% if childlink.active or childlink.child_active %}style="height: auto;"{% endif %}> <div class="collapsible-content__inner"> <ul class="mobile-nav__grandchildlist"> {%- for grandchildlink in childlink.links -%} <li class="mobile-nav__item"> <a href="{{ grandchildlink.url }}" class="mobile-nav__link"{% if grandchildlink.active %} data-active="true"{% endif %}> {{ grandchildlink.title }} </a> </li> {%- endfor -%} </ul> </div> </div> {%- endif -%} </li> {%- endfor -%} </ul> </div> </div> {%- endif -%} </li> {%- endfor -%} {%- assign have_secondary_list = false -%} {%- if section.settings.toolbar_menu != blank or shop.customer_accounts_enabled -%} {%- assign have_secondary_list = true -%} {%- endif -%} {%- if have_secondary_list -%} <li class="mobile-nav__item mobile-nav__item--secondary"> <div class="grid"> {%- if section.settings.toolbar_menu != blank -%} {%- for link in toolbar_menu.links -%} {%- assign animation_row = animation_row | plus: 1 -%} <div class="grid__item one-half appear-animation appear-delay-{{ animation_row }} medium-up--hide"> <a href="{{ link.url }}" class="mobile-nav__link">{{ link.title }}</a> </div> {%- endfor -%} {%- endif -%} {%- if shop.customer_accounts_enabled -%} {%- assign animation_row = animation_row | plus: 1 -%} <div class="grid__item one-half appear-animation appear-delay-{{ animation_row }}"> <a href="{{ routes.account_url }}" class="mobile-nav__link"> {%- if customer -%} {{ 'layout.customer.account' | t }} {%- else -%} {{ 'layout.customer.log_in' | t }} {%- endif -%} </a> </div> {%- endif -%} </div> </li> {%- endif -%} </ul> {%- assign animation_row = animation_row | plus: 1 -%} <ul class="mobile-nav__social appear-animation appear-delay-{{ animation_row }}"> {%- if settings.social_instagram_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_instagram_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'Instagram' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-instagram" viewBox="0 0 32 32"><path fill="#444" d="M16 3.094c4.206 0 4.7.019 6.363.094 1.538.069 2.369.325 2.925.544.738.287 1.262.625 1.813 1.175s.894 1.075 1.175 1.813c.212.556.475 1.387.544 2.925.075 1.662.094 2.156.094 6.363s-.019 4.7-.094 6.363c-.069 1.538-.325 2.369-.544 2.925-.288.738-.625 1.262-1.175 1.813s-1.075.894-1.813 1.175c-.556.212-1.387.475-2.925.544-1.663.075-2.156.094-6.363.094s-4.7-.019-6.363-.094c-1.537-.069-2.369-.325-2.925-.544-.737-.288-1.263-.625-1.813-1.175s-.894-1.075-1.175-1.813c-.212-.556-.475-1.387-.544-2.925-.075-1.663-.094-2.156-.094-6.363s.019-4.7.094-6.363c.069-1.537.325-2.369.544-2.925.287-.737.625-1.263 1.175-1.813s1.075-.894 1.813-1.175c.556-.212 1.388-.475 2.925-.544 1.662-.081 2.156-.094 6.363-.094zm0-2.838c-4.275 0-4.813.019-6.494.094-1.675.075-2.819.344-3.819.731-1.037.4-1.913.944-2.788 1.819S1.486 4.656 1.08 5.688c-.387 1-.656 2.144-.731 3.825-.075 1.675-.094 2.213-.094 6.488s.019 4.813.094 6.494c.075 1.675.344 2.819.731 3.825.4 1.038.944 1.913 1.819 2.788s1.756 1.413 2.788 1.819c1 .387 2.144.656 3.825.731s2.213.094 6.494.094 4.813-.019 6.494-.094c1.675-.075 2.819-.344 3.825-.731 1.038-.4 1.913-.944 2.788-1.819s1.413-1.756 1.819-2.788c.387-1 .656-2.144.731-3.825s.094-2.212.094-6.494-.019-4.813-.094-6.494c-.075-1.675-.344-2.819-.731-3.825-.4-1.038-.944-1.913-1.819-2.788s-1.756-1.413-2.788-1.819c-1-.387-2.144-.656-3.825-.731C20.812.275 20.275.256 16 .256z"/><path fill="#444" d="M16 7.912a8.088 8.088 0 0 0 0 16.175c4.463 0 8.087-3.625 8.087-8.088s-3.625-8.088-8.088-8.088zm0 13.338a5.25 5.25 0 1 1 0-10.5 5.25 5.25 0 1 1 0 10.5zM26.294 7.594a1.887 1.887 0 1 1-3.774.002 1.887 1.887 0 0 1 3.774-.003z"/></svg> <span class="icon__fallback-text">Instagram</span> </a> </li> {%- endif -%} {%- if settings.social_facebook_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_facebook_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'Facebook' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-facebook" viewBox="0 0 14222 14222"><path d="M14222 7112c0 3549.352-2600.418 6491.344-6000 7024.72V9168h1657l315-2056H8222V5778c0-562 275-1111 1159-1111h897V2917s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4968.72C2600.418 13603.344 0 10661.352 0 7112 0 3184.703 3183.703 1 7111 1s7111 3183.703 7111 7111zm-8222 7025c362 57 733 86 1111 86-377.945 0-749.003-29.485-1111-86.28zm2222 0v-.28a7107.458 7107.458 0 0 1-167.717 24.267A7407.158 7407.158 0 0 0 8222 14137zm-167.717 23.987C7745.664 14201.89 7430.797 14223 7111 14223c319.843 0 634.675-21.479 943.283-62.013z"/></svg> <span class="icon__fallback-text">Facebook</span> </a> </li> {%- endif -%} {%- if settings.social_youtube_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_youtube_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'YouTube' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-youtube" viewBox="0 0 21 20"><path fill="#444" d="M-.196 15.803q0 1.23.812 2.092t1.977.861h14.946q1.165 0 1.977-.861t.812-2.092V3.909q0-1.23-.82-2.116T17.539.907H2.593q-1.148 0-1.969.886t-.82 2.116v11.894zm7.465-2.149V6.058q0-.115.066-.18.049-.016.082-.016l.082.016 7.153 3.806q.066.066.066.164 0 .066-.066.131l-7.153 3.806q-.033.033-.066.033-.066 0-.098-.033-.066-.066-.066-.131z"/></svg> <span class="icon__fallback-text">YouTube</span> </a> </li> {%- endif -%} {%- if settings.social_twitter_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_twitter_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'Twitter' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-twitter" viewBox="0 0 32 32"><path fill="#444" d="M31.281 6.733q-1.304 1.924-3.13 3.26 0 .13.033.408t.033.408q0 2.543-.75 5.086t-2.282 4.858-3.635 4.108-5.053 2.869-6.341 1.076q-5.282 0-9.65-2.836.913.065 1.5.065 4.401 0 7.857-2.673-2.054-.033-3.668-1.255t-2.266-3.146q.554.13 1.206.13.88 0 1.663-.261-2.184-.456-3.619-2.184t-1.435-3.977v-.065q1.239.652 2.836.717-1.271-.848-2.021-2.233t-.75-2.983q0-1.63.815-3.195 2.38 2.967 5.754 4.678t7.319 1.907q-.228-.815-.228-1.434 0-2.608 1.858-4.45t4.532-1.842q1.304 0 2.51.522t2.054 1.467q2.152-.424 4.01-1.532-.685 2.217-2.771 3.488 1.989-.261 3.619-.978z"/></svg> <span class="icon__fallback-text">Twitter</span> </a> </li> {%- endif -%} {%- if settings.social_pinterest_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_pinterest_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'Pinterest' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-pinterest" viewBox="0 0 256 256"><path d="M0 128.002c0 52.414 31.518 97.442 76.619 117.239-.36-8.938-.064-19.668 2.228-29.393 2.461-10.391 16.47-69.748 16.47-69.748s-4.089-8.173-4.089-20.252c0-18.969 10.994-33.136 24.686-33.136 11.643 0 17.268 8.745 17.268 19.217 0 11.704-7.465 29.211-11.304 45.426-3.207 13.578 6.808 24.653 20.203 24.653 24.252 0 40.586-31.149 40.586-68.055 0-28.054-18.895-49.052-53.262-49.052-38.828 0-63.017 28.956-63.017 61.3 0 11.152 3.288 19.016 8.438 25.106 2.368 2.797 2.697 3.922 1.84 7.134-.614 2.355-2.024 8.025-2.608 10.272-.852 3.242-3.479 4.401-6.409 3.204-17.884-7.301-26.213-26.886-26.213-48.902 0-36.361 30.666-79.961 91.482-79.961 48.87 0 81.035 35.364 81.035 73.325 0 50.213-27.916 87.726-69.066 87.726-13.819 0-26.818-7.47-31.271-15.955 0 0-7.431 29.492-9.005 35.187-2.714 9.869-8.026 19.733-12.883 27.421a127.897 127.897 0 0 0 36.277 5.249c70.684 0 127.996-57.309 127.996-128.005C256.001 57.309 198.689 0 128.005 0 57.314 0 0 57.309 0 128.002z"/></svg> <span class="icon__fallback-text">Pinterest</span> </a> </li> {%- endif -%} {%- if settings.social_snapchat_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_snapchat_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'Snapchat' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-snapchat" viewBox="0 0 56.693 56.693"><path d="M28.66 51.683c-.128 0-.254-.004-.38-.01a3.24 3.24 0 0 1-.248.01c-2.944 0-4.834-1.336-6.661-2.628-1.262-.892-2.453-1.733-3.856-1.967a12.448 12.448 0 0 0-2.024-.17c-1.186 0-2.122.182-2.806.316-.415.081-.773.151-1.045.151-.285 0-.593-.061-.727-.519-.116-.397-.2-.78-.281-1.152-.209-.956-.357-1.544-.758-1.605-4.67-.722-6.006-1.705-6.304-2.403a.898.898 0 0 1-.072-.299.526.526 0 0 1 .44-.548c7.178-1.182 10.397-8.519 10.53-8.83l.012-.026c.44-.89.526-1.663.257-2.297-.493-1.16-2.1-1.67-3.163-2.008-.26-.082-.507-.16-.701-.237-2.123-.84-2.3-1.7-2.216-2.14.142-.747 1.142-1.268 1.95-1.268.222 0 .417.039.581.116.955.447 1.815.673 2.558.673 1.025 0 1.473-.43 1.528-.487-.026-.486-.059-.993-.092-1.517-.213-3.394-.478-7.61.595-10.018 3.218-7.215 10.043-7.776 12.057-7.776l.884-.009h.119c2.02 0 8.858.562 12.078 7.78 1.074 2.41.808 6.63.594 10.021l-.009.147c-.03.473-.058.932-.082 1.371.051.052.463.449 1.393.485h.001c.707-.028 1.52-.253 2.41-.67.262-.122.552-.148.75-.148.3 0 .607.058.86.164l.016.007c.721.255 1.193.76 1.204 1.289.009.497-.37 1.244-2.233 1.98-.193.076-.44.154-.7.237-1.065.338-2.671.848-3.164 2.008-.269.633-.183 1.406.257 2.297l.011.026c.134.311 3.35 7.646 10.532 8.83.265.043.454.28.44.548a.884.884 0 0 1-.074.3c-.296.693-1.632 1.675-6.303 2.397-.381.059-.53.556-.757 1.599-.083.38-.167.752-.282 1.144-.1.34-.312.5-.668.5h-.058c-.248 0-.6-.045-1.046-.133-.79-.154-1.677-.297-2.805-.297-.659 0-1.34.058-2.026.171-1.401.234-2.591 1.074-3.85 1.964-1.831 1.295-3.72 2.63-6.666 2.63z"/></svg> <span class="icon__fallback-text">Snapchat</span> </a> </li> {%- endif -%} {%- if settings.social_tiktok_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_tiktok_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'TickTok' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-tiktok" viewBox="0 0 2859 3333"><path d="M2081 0c55 473 319 755 778 785v532c-266 26-499-61-770-225v995c0 1264-1378 1659-1932 753-356-583-138-1606 1004-1647v561c-87 14-180 36-265 65-254 86-398 247-358 531 77 544 1075 705 992-358V1h551z"/></svg> <span class="icon__fallback-text">TikTok</span> </a> </li> {%- endif -%} {%- if settings.social_tumblr_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_tumblr_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'Tumblr' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-tumblr" viewBox="0 0 32 32"><path fill="#444" d="M6.997 13.822h3.022v10.237q0 1.852.414 3.047.463 1.097 1.438 1.95.951.877 2.511 1.438 1.487.512 3.388.512 1.657 0 3.096-.366 1.243-.244 3.242-1.194v-4.582q-2.023 1.389-4.192 1.389-1.072 0-2.145-.561-.634-.414-.951-1.146-.244-.804-.244-3.242v-7.483h6.581V9.239h-6.581V1.902h-3.949q-.195 2.072-.951 3.681-.756 1.56-1.901 2.486Q8.581 9.19 6.996 9.678v4.144z"/></svg> <span class="icon__fallback-text">Tumblr</span> </a> </li> {%- endif -%} {%- if settings.social_linkedin_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_linkedin_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'LinkedIn' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-linkedin" viewBox="0 0 24 24"><path d="M4.98 3.5C4.98 4.881 3.87 6 2.5 6S.02 4.881.02 3.5C.02 2.12 1.13 1 2.5 1s2.48 1.12 2.48 2.5zM5 8H0v16h5V8zm7.982 0H8.014v16h4.969v-8.399c0-4.67 6.029-5.052 6.029 0V24H24V13.869c0-7.88-8.922-7.593-11.018-3.714V8z"/></svg> <span class="icon__fallback-text">LinkedIn</span> </a> </li> {%- endif -%} {%- if settings.social_vimeo_link != blank -%} <li class="mobile-nav__social-item"> <a target="_blank" rel="noopener" href="{{ settings.social_vimeo_link }}" title="{{ 'layout.footer.social_platform' | t: name: shop.name, platform: 'Vimeo' }}"> <svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-vimeo" viewBox="0 0 32 32"><path fill="#444" d="M.343 10.902l1.438 1.926q1.999-1.487 2.413-1.487 1.584 0 2.949 5.046l1.194 4.521q.828 3.132 1.292 4.814 1.804 5.046 4.534 5.046 4.339 0 10.53-8.336 6.069-7.922 6.288-12.528v-.536q0-5.606-4.485-5.752h-.341q-6.02 0-8.287 7.385 1.316-.561 2.291-.561 2.072 0 2.072 2.145 0 .268-.024.561-.146 1.731-2.047 4.729-1.95 3.144-2.901 3.144-1.267 0-2.242-4.777-.293-1.121-1.243-7.239-.414-2.632-1.536-3.9-.975-1.097-2.437-1.121-.195 0-.414.024-1.536.146-4.558 2.803-1.56 1.462-4.485 4.095z"/></svg> <span class="icon__fallback-text">Vimeo</span> </a> </li> {%- endif -%} </ul> </div> </div> </div> /* The theme.js file */ /* @license Impulse by Archetype Themes (https://archetypethemes.co) Access unminified JS in assets/theme.js Use this event listener to run your own JS outside of this file. Documentation - https://archetypethemes.co/blogs/impulse/javascript-events-for-developers document.addEventListener('page:loaded', function() { // Page has loaded and theme assets are ready }); */ window.theme = window.theme || {}; window.Shopify = window.Shopify || {}; theme.config = { bpSmall: false, hasSessionStorage: true, hasLocalStorage: true, mediaQuerySmall: 'screen and (max-width: '+ 769 +'px)', youTubeReady: false, vimeoReady: false, vimeoLoading: false, isTouch: ('ontouchstart' in window) || window.DocumentTouch && window.document instanceof DocumentTouch || window.navigator.maxTouchPoints || window.navigator.msMaxTouchPoints ? true : false, stickyHeader: false, rtl: document.documentElement.getAttribute('dir') == 'rtl' ? true : false }; if (theme.config.isTouch) { document.documentElement.className += ' supports-touch'; } if (console && console.log) { console.log('Impulse theme ('+theme.settings.themeVersion+') by ARCHΞTYPE | Learn more at https://archetypethemes.co'); } theme.recentlyViewed = { recent: {}, // will store handle+url of recent products productInfo: {} // will store product data to reduce API calls }; (function(){ 'use strict'; theme.delegate = { on: function(event, callback, options){ if( !this.namespaces ) // save the namespaces on the DOM element itself this.namespaces = {}; this.namespaces[event] = callback; options = options || false; this.addEventListener(event.split('.')[0], callback, options); return this; }, off: function(event) { if (!this.namespaces) { return } this.removeEventListener(event.split('.')[0], this.namespaces[event]); delete this.namespaces[event]; return this; } }; // Extend the DOM with these above custom methods window.on = Element.prototype.on = theme.delegate.on; window.off = Element.prototype.off = theme.delegate.off; theme.utils = { defaultTo: function(value, defaultValue) { return (value == null || value !== value) ? defaultValue : value }, wrap: function(el, wrapper) { el.parentNode.insertBefore(wrapper, el); wrapper.appendChild(el); }, debounce: function(wait, callback, immediate) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) callback.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) callback.apply(context, args); } }, throttle: function(limit, callback) { var waiting = false; return function () { if (!waiting) { callback.apply(this, arguments); waiting = true; setTimeout(function () { waiting = false; }, limit); } } }, prepareTransition: function(el, callback) { el.addEventListener('transitionend', removeClass); function removeClass(evt) { el.classList.remove('is-transitioning'); el.removeEventListener('transitionend', removeClass); } el.classList.add('is-transitioning'); el.offsetWidth; // check offsetWidth to force the style rendering if (typeof callback === 'function') { callback(); } }, // _.compact from lodash // Creates an array with all falsey values removed. The values `false`, `null`, // `0`, `""`, `undefined`, and `NaN` are falsey. // _.compact([0, 1, false, 2, '', 3]); // => [1, 2, 3] compact: function(array) { var index = -1, length = array == null ? 0 : array.length, resIndex = 0, result = []; while (++index < length) { var value = array[index]; if (value) { result[resIndex++] = value; } } return result; }, serialize: function(form) { var arr = []; Array.prototype.slice.call(form.elements).forEach(function(field) { if ( !field.name || field.disabled || ['file', 'reset', 'submit', 'button'].indexOf(field.type) > -1 ) return; if (field.type === 'select-multiple') { Array.prototype.slice.call(field.options).forEach(function(option) { if (!option.selected) return; arr.push( encodeURIComponent(field.name) + '=' + encodeURIComponent(option.value) ); }); return; } if (['checkbox', 'radio'].indexOf(field.type) > -1 && !field.checked) return; arr.push( encodeURIComponent(field.name) + '=' + encodeURIComponent(field.value) ); }); return arr.join('&'); } }; theme.a11y = { trapFocus: function(options) { var eventsName = { focusin: options.namespace ? 'focusin.' + options.namespace : 'focusin', focusout: options.namespace ? 'focusout.' + options.namespace : 'focusout', keydown: options.namespace ? 'keydown.' + options.namespace : 'keydown.handleFocus' }; // Get every possible visible focusable element var focusableEls = options.container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex^="-"])'); var elArray = [].slice.call(focusableEls); var focusableElements = elArray.filter(el => el.offsetParent !== null); var firstFocusable = focusableElements[0]; var lastFocusable = focusableElements[focusableElements.length - 1]; if (!options.elementToFocus) { options.elementToFocus = options.container; } options.container.setAttribute('tabindex', '-1'); options.elementToFocus.focus(); document.documentElement.off('focusin'); document.documentElement.on(eventsName.focusout, function() { document.documentElement.off(eventsName.keydown); }); document.documentElement.on(eventsName.focusin, function(evt) { if (evt.target !== lastFocusable && evt.target !== firstFocusable) return; document.documentElement.on(eventsName.keydown, function(evt) { _manageFocus(evt); }); }); function _manageFocus(evt) { if (evt.keyCode !== 9) return; /** * On the first focusable element and tab backward, * focus the last element */ if (evt.target === firstFocusable && evt.shiftKey) { evt.preventDefault(); lastFocusable.focus(); } } }, removeTrapFocus: function(options) { var eventName = options.namespace ? 'focusin.' + options.namespace : 'focusin'; if (options.container) { options.container.removeAttribute('tabindex'); } document.documentElement.off(eventName); }, lockMobileScrolling: function(namespace, element) { var el = element ? element : document.documentElement; document.documentElement.classList.add('lock-scroll'); el.on('touchmove' + namespace, function() { return true; }); }, unlockMobileScrolling: function(namespace, element) { document.documentElement.classList.remove('lock-scroll'); var el = element ? element : document.documentElement; el.off('touchmove' + namespace); } }; // Add class when tab key starts being used to show outlines document.documentElement.on('keyup.tab', function(evt) { if (evt.keyCode === 9) { document.documentElement.classList.add('tab-outline'); document.documentElement.off('keyup.tab'); } }); /** * Currency Helpers * ----------------------------------------------------------------------------- * A collection of useful functions that help with currency formatting * * Current contents * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value. * - When theme.settings.superScriptPrice is enabled, format cents in <sup> tag * - getBaseUnit - Splits unit price apart to get value + unit * */ theme.Currency = (function() { var moneyFormat = '${{amount}}'; var superScript = theme && theme.settings && theme.settings.superScriptPrice; function formatMoney(cents, format) { if (!format) { format = theme.settings.moneyFormat; } if (typeof cents === 'string') { cents = cents.replace('.', ''); } var value = ''; var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/; var formatString = (format || moneyFormat); function formatWithDelimiters(number, precision, thousands, decimal) { precision = theme.utils.defaultTo(precision, 2); thousands = theme.utils.defaultTo(thousands, ','); decimal = theme.utils.defaultTo(decimal, '.'); if (isNaN(number) || number == null) { return 0; } number = (number / 100.0).toFixed(precision); var parts = number.split('.'); var dollarsAmount = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands); var centsAmount = parts[1] ? (decimal + parts[1]) : ''; return dollarsAmount + centsAmount; } switch (formatString.match(placeholderRegex)[1]) { case 'amount': value = formatWithDelimiters(cents, 2); if (superScript && value && value.includes('.')) { value = value.replace('.', '<sup>') + '</sup>'; } break; case 'amount_no_decimals': value = formatWithDelimiters(cents, 0); break; case 'amount_with_comma_separator': value = formatWithDelimiters(cents, 2, '.', ','); if (superScript && value && value.includes(',')) { value = value.replace(',', '<sup>') + '</sup>'; } break; case 'amount_no_decimals_with_comma_separator': value = formatWithDelimiters(cents, 0, '.', ','); break; case 'amount_no_decimals_with_space_separator': value = formatWithDelimiters(cents, 0, ' '); break; } return formatString.replace(placeholderRegex, value); } function getBaseUnit(variant) { if (!variant) { return; } if (!variant.unit_price_measurement || !variant.unit_price_measurement.reference_value) { return; } return variant.unit_price_measurement.reference_value === 1 ? variant.unit_price_measurement.reference_unit : variant.unit_price_measurement.reference_value + variant.unit_price_measurement.reference_unit; } return { formatMoney: formatMoney, getBaseUnit: getBaseUnit } })(); theme.Images = (function() { /** * Find the Shopify image attribute size */ function imageSize(src) { if (!src) { return '620x'; // default based on theme } var match = src.match(/.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\.@]/); if (match !== null) { return match[1]; } else { return null; } } /** * Adds a Shopify size attribute to a URL */ function getSizedImageUrl(src, size) { if (!src) { return src; } if (size == null) { return src; } if (size === 'master') { return this.removeProtocol(src); } var match = src.match(/\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i); if (match != null) { var prefix = src.split(match[0]); var suffix = match[0]; return this.removeProtocol(prefix[0] + '_' + size + suffix); } return null; } function removeProtocol(path) { return path.replace(/http(s)?:/, ''); } function buildImagePath(string, widths) { if (string == null) return []; if (widths) { const imageUrls = []; widths.forEach(width => { let url = `${string}?width=${width}`; if (width === widths[widths.length - 1]) { url += ` ${width}w` } else { url += ` ${width}w,` } imageUrls.push(url); }) return imageUrls; } else { return [string]; } } return { imageSize: imageSize, getSizedImageUrl: getSizedImageUrl, removeProtocol: removeProtocol, buildImagePath: buildImagePath }; })(); // Init section function when it's visible, then disable observer theme.initWhenVisible = function(options) { var threshold = options.threshold ? options.threshold : 0; var observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { if (typeof options.callback === 'function') { options.callback(); observer.unobserve(entry.target); } } }); }, {rootMargin: '0px 0px '+ threshold +'px 0px'}); observer.observe(options.element); }; theme.LibraryLoader = (function() { var types = { link: 'link', script: 'script' }; var status = { requested: 'requested', loaded: 'loaded' }; var cloudCdn = 'https://cdn.shopify.com/shopifycloud/'; var libraries = { youtubeSdk: { tagId: 'youtube-sdk', src: 'https://www.youtube.com/iframe_api', type: types.script }, vimeo: { tagId: 'vimeo-api', src: 'https://player.vimeo.com/api/player.js', type: types.script }, shopifyXr: { tagId: 'shopify-model-viewer-xr', src: cloudCdn + 'shopify-xr-js/assets/v1.0/shopify-xr.en.js', type: types.script }, modelViewerUi: { tagId: 'shopify-model-viewer-ui', src: cloudCdn + 'model-viewer-ui/assets/v1.0/model-viewer-ui.en.js', type: types.script }, modelViewerUiStyles: { tagId: 'shopify-model-viewer-ui-styles', src: cloudCdn + 'model-viewer-ui/assets/v1.0/model-viewer-ui.css', type: types.link } }; function load(libraryName, callback) { var library = libraries[libraryName]; if (!library) return; if (library.status === status.requested) return; callback = callback || function() {}; if (library.status === status.loaded) { callback(); return; } library.status = status.requested; var tag; switch (library.type) { case types.script: tag = createScriptTag(library, callback); break; case types.link: tag = createLinkTag(library, callback); break; } tag.id = library.tagId; library.element = tag; var firstScriptTag = document.getElementsByTagName(library.type)[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); } function createScriptTag(library, callback) { var tag = document.createElement('script'); tag.src = library.src; tag.addEventListener('load', function() { library.status = status.loaded; callback(); }); return tag; } function createLinkTag(library, callback) { var tag = document.createElement('link'); tag.href = library.src; tag.rel = 'stylesheet'; tag.type = 'text/css'; tag.addEventListener('load', function() { library.status = status.loaded; callback(); }); return tag; } return { load: load }; })(); theme.rteInit = function() { // Wrap tables so they become scrollable on small screens document.querySelectorAll('.rte table').forEach(table => { var wrapWith = document.createElement('div'); wrapWith.classList.add('table-wrapper'); theme.utils.wrap(table, wrapWith); }); // Wrap video iframe embeds so they are responsive document.querySelectorAll('.rte iframe[src*="youtube.com/embed"]').forEach(iframe => { wrapVideo(iframe); }); document.querySelectorAll('.rte iframe[src*="player.vimeo"]').forEach(iframe => { wrapVideo(iframe); }); function wrapVideo(iframe) { // Reset the src attribute on each iframe after page load // for Chrome's "incorrect iFrame content on 'back'" bug. // https://code.google.com/p/chromium/issues/detail?id=395791 iframe.src = iframe.src; var wrapWith = document.createElement('div'); wrapWith.classList.add('video-wrapper'); theme.utils.wrap(iframe, wrapWith); } // Remove CSS that adds animated underline under image links document.querySelectorAll('.rte a img').forEach(img => { img.parentNode.classList.add('rte__image'); }); } theme.Sections = function Sections() { this.constructors = {}; this.instances = []; document.addEventListener('shopify:section:load', this._onSectionLoad.bind(this)); document.addEventListener('shopify:section:unload', this._onSectionUnload.bind(this)); document.addEventListener('shopify:section:select', this._onSelect.bind(this)); document.addEventListener('shopify:section:deselect', this._onDeselect.bind(this)); document.addEventListener('shopify:block:select', this._onBlockSelect.bind(this)); document.addEventListener('shopify:block:deselect', this._onBlockDeselect.bind(this)); }; theme.Sections.prototype = Object.assign({}, theme.Sections.prototype, { _createInstance: function(container, constructor, scope) { var id = container.getAttribute('data-section-id'); var type = container.getAttribute('data-section-type'); constructor = constructor || this.constructors[type]; if (typeof constructor === 'undefined') { return; } // If custom scope passed, check to see if instance // is already initialized so we don't double up if (scope) { var instanceExists = this._findInstance(id); if (instanceExists) { this._removeInstance(id); } } // If a section fails to init, handle the error without letting all subsequest section registers to fail try { var instance = Object.assign(new constructor(container), { id: id, type: type, container: container }); this.instances.push(instance); } catch (e) { console.error(e); } }, _findInstance: function(id) { for (var i = 0; i < this.instances.length; i++) { if (this.instances[i].id === id) { return this.instances[i]; } } }, _removeInstance: function(id) { var i = this.instances.length; var instance; while(i--) { if (this.instances[i].id === id) { instance = this.instances[i]; this.instances.splice(i, 1); break; } } return instance; }, _onSectionLoad: function(evt, subSection, subSectionId) { if (window.AOS) { AOS.refreshHard() } if (theme && theme.initGlobals) { theme.initGlobals(); } var container = subSection ? subSection : evt.target; var section = subSection ? subSection : evt.target.querySelector('[data-section-id]'); if (!section) { return; } this._createInstance(section); var instance = subSection ? subSectionId : this._findInstance(evt.detail.sectionId); // Check if we have subsections to load var haveSubSections = container.querySelectorAll('[data-subsection]'); if (haveSubSections.length) { this.loadSubSections(container); } // Run JS only in case of the section being selected in the editor // before merchant clicks "Add" if (instance && typeof instance.onLoad === 'function') { instance.onLoad(evt); } // Force editor to trigger scroll event when loading a section setTimeout(function() { window.dispatchEvent(new Event('scroll')); }, 200); }, _onSectionUnload: function(evt) { this.instances = this.instances.filter(function(instance) { var isEventInstance = instance.id === evt.detail.sectionId; if (isEventInstance) { if (typeof instance.onUnload === 'function') { instance.onUnload(evt); } } return !isEventInstance; }); }, loadSubSections: function(scope) { if (!scope) { return; } var sections = scope.querySelectorAll('[data-section-id]'); sections.forEach(el => { this._onSectionLoad(null, el, el.dataset.sectionId); }); }, _onSelect: function(evt) { var instance = this._findInstance(evt.detail.sectionId); if ( typeof instance !== 'undefined' && typeof instance.onSelect === 'function' ) { instance.onSelect(evt); } }, _onDeselect: function(evt) { var instance = this._findInstance(evt.detail.sectionId); if ( typeof instance !== 'undefined' && typeof instance.onDeselect === 'function' ) { instance.onDeselect(evt); } }, _onBlockSelect: function(evt) { var instance = this._findInstance(evt.detail.sectionId); if ( typeof instance !== 'undefined' && typeof instance.onBlockSelect === 'function' ) { instance.onBlockSelect(evt); } }, _onBlockDeselect: function(evt) { var instance = this._findInstance(evt.detail.sectionId); if ( typeof instance !== 'undefined' && typeof instance.onBlockDeselect === 'function' ) { instance.onBlockDeselect(evt); } }, register: function(type, constructor, scope) { this.constructors[type] = constructor; var sections = document.querySelectorAll('[data-section-type="' + type + '"]'); if (scope) { sections = scope.querySelectorAll('[data-section-type="' + type + '"]'); } sections.forEach( function(container) { this._createInstance(container, constructor, scope); }.bind(this) ); }, reinit: function(section) { for (var i = 0; i < this.instances.length; i++) { var instance = this.instances[i]; if (instance['type'] === section) { if (typeof instance.forceReload === 'function') { instance.forceReload(); } } } } }); /* Options: container enableHistoryState - enable when on single product page to update URL singleOptionSelector - selector for individual variant option (e.g. 'Blue' or 'Small') originalSelectorId - selector for base variant selector (visually hidden) variants - JSON parsed object of product variant info */ theme.Variants = (function() { function Variants(options) { this.container = options.container; this.variants = options.variants; this.singleOptionSelector = options.singleOptionSelector; this.originalSelectorId = options.originalSelectorId; this.enableHistoryState = options.enableHistoryState; this.dynamicVariantsEnabled = options.dynamicVariantsEnabled; this.currentlySelectedValues = this._getCurrentOptions(); this.currentVariant = this._getVariantFromOptions(); this.container.querySelectorAll(this.singleOptionSelector).forEach(el => { el.addEventListener('change', this._onSelectChange.bind(this)); }); } Variants.prototype = Object.assign({}, Variants.prototype, { _getCurrentOptions: function() { var result = []; this.container.querySelectorAll(this.singleOptionSelector).forEach(el => { var type = el.getAttribute('type'); if (type === 'radio' || type === 'checkbox') { if (el.checked) { result.push({ value: el.value, index: el.dataset.index }); } } else { result.push({ value: el.value, index: el.dataset.index }); } }); // remove any unchecked input values if using radio buttons or checkboxes result = theme.utils.compact(result); return result; }, // Pull the number out of the option index name, e.g. 'option1' -> 1 _numberFromOptionKey: function(key) { return parseInt(key.substr(-1)); }, // Options should be ordered from highest to lowest priority. Make sure that priority // is represented using weighted values when finding best match _getWeightedOptionMatchCount: function(variant) { return this._getCurrentOptions().reduce((count, {value, index}) => { const optionIndex = this._numberFromOptionKey(index); const weightedCount = 4 - optionIndex; // The lower the index, the better the match we have return variant[index] === value ? count + weightedCount : count; },0) }, _getFullMatch(needsToBeAvailable) { const currentlySelectedOptions = this._getCurrentOptions(); const variants = this.variants; return variants.find(variant => { const isMatch = currentlySelectedOptions.every(({value, index}) => { return variant[index] === value; }); if (needsToBeAvailable) { return isMatch && variant.available; } else { return isMatch; } }); }, // Find a variant that is available and best matches last selected option _getClosestAvailableMatch: function(lastSelectedOption) { if (!lastSelectedOption) return null; const currentlySelectedOptions = this._getCurrentOptions(); const variants = this.variants; const potentialAvailableMatches = lastSelectedOption && variants.filter(variant => { return currentlySelectedOptions .filter( // Only match based selected options that are equal and preceeding the last selected option ({value, index}) => this._numberFromOptionKey(index) <= this._numberFromOptionKey(lastSelectedOption.index) ).every(({value, index}) => { // Variant needs to have options that match the current and preceeding selection options return variant[index] === value; }) && variant.available }); return potentialAvailableMatches.reduce((bestMatch, variant) => { // If this is the first potential match we've found, store it as the best match if (bestMatch === null) return variant; // If this is not the first potential match, compare the number of options our current best match has in common // compared to the next contender. const bestMatchCount = this._getWeightedOptionMatchCount(bestMatch, lastSelectedOption); const newCount = this._getWeightedOptionMatchCount(variant, lastSelectedOption); return newCount > bestMatchCount ? variant : bestMatch; }, null); }, _getVariantFromOptions: function(lastSelectedOption) { const availableFullMatch = this._getFullMatch(true); const closestAvailableMatch = this._getClosestAvailableMatch(lastSelectedOption); const fullMatch = this._getFullMatch(false); if (this.dynamicVariantsEnabled) { // Add some additional smarts to variant matching if Dynamic Variants are enabled return availableFullMatch || closestAvailableMatch || fullMatch || null; } else { // Only return a full match or null (variant doesn't exist) if Dynamic Variants are disabled return fullMatch || null; } }, _updateInputState: function (variant, el) { return (input) => { if (variant === null) return; const index = input.dataset.index; const value = input.value; const type = input.getAttribute('type'); if (type === 'radio' || type === 'checkbox') { input.checked = variant[index] === value } else { input.value = variant[index]; } } }, _onSelectChange: function({srcElement}) { const optionSelectElements = this.container.querySelectorAll(this.singleOptionSelector); // Get the best variant based on the current selection + last selected element const variant = this._getVariantFromOptions({ index: srcElement.dataset.index, value: srcElement.value }); // Update DOM option input states based on the variant that was found optionSelectElements.forEach(this._updateInputState(variant, srcElement)) // Make sure our currently selected values are up to date after updating state of DOM const currentlySelectedValues = this.currentlySelectedValues = this._getCurrentOptions(); const detail = { variant, currentlySelectedValues, value: srcElement.value, index: srcElement.parentElement.dataset.index } this.container.dispatchEvent(new CustomEvent('variantChange', {detail})); document.dispatchEvent(new CustomEvent('variant:change', {detail})); if (!variant) { return; } this._updateMasterSelect(variant); this._updateImages(variant); this._updatePrice(variant); this._updateUnitPrice(variant); this._updateSKU(variant); this.currentVariant = variant; if (this.enableHistoryState) { this._updateHistoryState(variant); } }, _updateImages: function(variant) { var variantImage = variant.featured_image || {}; var currentVariantImage = this.currentVariant && this.currentVariant.featured_image || {}; if (!variant.featured_image || variantImage.src === currentVariantImage.src) { return; } this.container.dispatchEvent(new CustomEvent('variantImageChange', { detail: { variant: variant } })); }, _updatePrice: function(variant) { if (this.currentVariant && variant.price === this.currentVariant.price && variant.compare_at_price === this.currentVariant.compare_at_price) { return; } this.container.dispatchEvent(new CustomEvent('variantPriceChange', { detail: { variant: variant } })); }, _updateUnitPrice: function(variant) { if (this.currentVariant && variant.unit_price === this.currentVariant.unit_price) { return; } this.container.dispatchEvent(new CustomEvent('variantUnitPriceChange', { detail: { variant: variant } })); }, _updateSKU: function(variant) { if (this.currentVariant && variant.sku === this.currentVariant.sku) { return; } this.container.dispatchEvent(new CustomEvent('variantSKUChange', { detail: { variant: variant } })); }, _updateHistoryState: function(variant) { if (!history.replaceState || !variant) { return; } var newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant.id; window.history.replaceState({path: newurl}, '', newurl); }, _updateMasterSelect: function(variant) { let masterSelect = this.container.querySelector(this.originalSelectorId); if (!masterSelect) return; masterSelect.value = variant.id; // Force a change event so Shop Pay installments works after a variant is changed masterSelect.dispatchEvent(new Event('change', { bubbles: true })); } }); return Variants; })(); window.vimeoApiReady = function() { theme.config.vimeoLoading = true; // Because there's no way to check for the Vimeo API being loaded // asynchronously, we use this terrible timeout to wait for it being ready checkIfVimeoIsReady() .then(function() { theme.config.vimeoReady = true; theme.config.vimeoLoading = false; document.dispatchEvent(new CustomEvent('vimeoReady')); }); } function checkIfVimeoIsReady() { var wait; var timeout; var deferred = new Promise((resolve, reject) => { wait = setInterval(function() { if (!Vimeo) { return; } clearInterval(wait); clearTimeout(timeout); resolve(); }, 500); timeout = setTimeout(function() { clearInterval(wait); reject(); }, 4000); // subjective. test up to 8 times over 4 seconds }); return deferred; } theme.VimeoPlayer = (function() { var classes = { loading: 'loading', loaded: 'loaded', interactable: 'video-interactable' } var defaults = { byline: false, loop: true, muted: true, playsinline: true, portrait: false, title: false }; function VimeoPlayer(divId, videoId, options) { this.divId = divId; this.el = document.getElementById(divId); this.videoId = videoId; this.iframe = null; this.options = options; if (this.options && this.options.videoParent) { this.parent = this.el.closest(this.options.videoParent); } this.setAsLoading(); if (theme.config.vimeoReady) { this.init(); } else { theme.LibraryLoader.load('vimeo', window.vimeoApiReady); document.addEventListener('vimeoReady', this.init.bind(this)); } } VimeoPlayer.prototype = Object.assign({}, VimeoPlayer.prototype, { init: function() { var args = defaults; args.id = this.videoId; this.videoPlayer = new Vimeo.Player(this.el, args); this.videoPlayer.ready().then(this.playerReady.bind(this)); }, playerReady: function() { this.iframe = this.el.querySelector('iframe'); this.iframe.setAttribute('tabindex', '-1'); if (this.options.loop === 'false') { this.videoPlayer.setLoop(false); } // When sound is enabled in section settings, // for some mobile browsers Vimeo video playback // will stop immediately after starting and // will require users to tap the play button once more if (this.options.style === 'sound') { this.videoPlayer.setVolume(1); } else { this.videoPlayer.setVolume(0); } this.setAsLoaded(); // pause when out of view var observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { this.play(); } else { this.pause(); } }); }, {rootMargin: '0px 0px 50px 0px'}); observer.observe(this.iframe); }, setAsLoading: function() { if (!this.parent) return; this.parent.classList.add(classes.loading); }, setAsLoaded: function() { if (!this.parent) return; this.parent.classList.remove(classes.loading); this.parent.classList.add(classes.loaded); this.parent.classList.add(classes.interactable); // Once video is loaded, we should be able to interact with it if (Shopify && Shopify.designMode) { if (window.AOS) {AOS.refreshHard()} } }, enableInteraction: function() { if (!this.parent) return; this.parent.classList.add(classes.interactable); }, play: function() { if (this.videoPlayer && typeof this.videoPlayer.play === 'function') { this.videoPlayer.play(); } }, pause: function() { if (this.videoPlayer && typeof this.videoPlayer.pause === 'function') { this.videoPlayer.pause(); } }, destroy: function() { if (this.videoPlayer && typeof this.videoPlayer.destroy === 'function') { this.videoPlayer.destroy(); } } }); return VimeoPlayer; })(); window.onYouTubeIframeAPIReady = function() { theme.config.youTubeReady = true; document.dispatchEvent(new CustomEvent('youTubeReady')); } /*============================================================================ YouTube SDK method Parameters: - player div id (required) - arguments - videoId (required) - videoParent (selector, optional for section loading state) - events (object, optional) ==============================================================================*/ theme.YouTube = (function() { var classes = { loading: 'loading', loaded: 'loaded', interactable: 'video-interactable' } var defaults = { width: 1280, height: 720, playerVars: { autohide: 0, autoplay: 1, cc_load_policy: 0, controls: 0, fs: 0, iv_load_policy: 3, modestbranding: 1, playsinline: 1, rel: 0 } }; function YouTube(divId, options) { this.divId = divId; this.iframe = null; this.attemptedToPlay = false; // API callback events defaults.events = { onReady: this.onVideoPlayerReady.bind(this), onStateChange: this.onVideoStateChange.bind(this) }; this.options = Object.assign({}, defaults, options); if (this.options) { if (this.options.videoParent) { this.parent = document.getElementById(this.divId).closest(this.options.videoParent); } // Most YT videos will autoplay. If in product media, // will handle in theme.Product instead if (!this.options.autoplay) { this.options.playerVars.autoplay = this.options.autoplay; } if (this.options.style === 'sound') { this.options.playerVars.controls = 1; this.options.playerVars.autoplay = 0; } } this.setAsLoading(); if (theme.config.youTubeReady) { this.init(); } else { theme.LibraryLoader.load('youtubeSdk'); document.addEventListener('youTubeReady', this.init.bind(this)); } } YouTube.prototype = Object.assign({}, YouTube.prototype, { init: function() { this.videoPlayer = new YT.Player(this.divId, this.options); }, onVideoPlayerReady: function(evt) { this.iframe = document.getElementById(this.divId); // iframe once YT loads this.iframe.setAttribute('tabindex', '-1'); if (this.options.style !== 'sound') { evt.target.mute(); } // pause when out of view var observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { this.play(); } else { this.pause(); } }); }, {rootMargin: '0px 0px 50px 0px'}); observer.observe(this.iframe); }, onVideoStateChange: function(evt) { switch (evt.data) { case -1: // unstarted // Handle low power state on iOS by checking if // video is reset to unplayed after attempting to buffer if (this.attemptedToPlay) { this.setAsLoaded(); this.enableInteraction(); } break; case 0: // ended, loop it this.play(evt); break; case 1: // playing this.setAsLoaded(); break; case 3: // buffering this.attemptedToPlay = true; break; } }, setAsLoading: function() { if (!this.parent) return; this.parent.classList.add(classes.loading); }, setAsLoaded: function() { if (!this.parent) return; this.parent.classList.remove(classes.loading); this.parent.classList.add(classes.loaded); if (Shopify && Shopify.designMode) { if (window.AOS) {AOS.refreshHard()} } }, enableInteraction: function() { if (!this.parent) return; this.parent.classList.add(classes.interactable); }, play: function() { if (this.videoPlayer && typeof this.videoPlayer.playVideo === 'function') { this.videoPlayer.playVideo(); } }, pause: function() { if (this.videoPlayer && typeof this.videoPlayer.pauseVideo === 'function') { this.videoPlayer.pauseVideo(); } }, destroy: function() { if (this.videoPlayer && typeof this.videoPlayer.destroy === 'function') { this.videoPlayer.destroy(); } } }); return YouTube; })(); // Prevent vertical scroll while using flickity sliders (function() { var e = !1; var t; document.body.addEventListener('touchstart', function(i) { if (!i.target.closest('.flickity-slider')) { return e = !1; void 0; } e = !0; t = { x: i.touches[0].pageX, y: i.touches[0].pageY } }) document.body.addEventListener('touchmove', function(i) { if (e && i.cancelable) { var n = { x: i.touches[0].pageX - t.x, y: i.touches[0].pageY - t.y }; Math.abs(n.x) > Flickity.defaults.dragThreshold && i.preventDefault() } }, { passive: !1 }) })(); /** * Ajax Renderer * ----------------------------------------------------------------------------- * Render sections without reloading the page. * @param {Object[]} sections - The section to update on render. * @param {string} sections[].sectionId - The ID of the section from Shopify. * @param {string} sections[].nodeId - The ID of the DOM node to replace. * @param {Function} sections[].onReplace (optional) - The custom render function. * @param {boolean} debug - Output logs to console for debugging. * */ theme.AjaxRenderer = (function () { function AjaxRenderer({ sections, onReplace, debug } = {}) { this.sections = sections || []; this.cachedSections = []; this.onReplace = onReplace; this.debug = Boolean(debug); } AjaxRenderer.prototype = Object.assign({}, AjaxRenderer.prototype, { renderPage: function (basePath, newParams, updateURLHash = true) { const currentParams = new URLSearchParams(window.location.search); const updatedParams = this.getUpdatedParams(currentParams, newParams) const sectionRenders = this.sections.map(section => { const url = `${basePath}?section_id=${section.sectionId}&${updatedParams.toString()}`; const cachedSectionUrl = cachedSection => cachedSection.url === url; return this.cachedSections.some(cachedSectionUrl) ? this.renderSectionFromCache(cachedSectionUrl, section) : this.renderSectionFromFetch(url, section); }); if (updateURLHash) this.updateURLHash(updatedParams); return Promise.all(sectionRenders); }, renderSectionFromCache: function (url, section) { const cachedSection = this.cachedSections.find(url); this.log(`[AjaxRenderer] rendering from cache: url=${cachedSection.url}`); this.renderSection(cachedSection.html, section); return Promise.resolve(section); }, renderSectionFromFetch: function (url, section) { this.log(`[AjaxRenderer] redering from fetch: url=${url}`); return new Promise((resolve, reject) => { fetch(url) .then(response => response.text()) .then(responseText => { const html = responseText; this.cachedSections = [...this.cachedSections, { html, url }]; this.renderSection(html, section); resolve(section); }) .catch(err => reject(err)); }); }, renderSection: function (html, section) { this.log( `[AjaxRenderer] rendering section: section=${JSON.stringify(section)}`, ); const newDom = new DOMParser().parseFromString(html, 'text/html'); if (this.onReplace) { this.onReplace(newDom, section); } else { if (typeof section.nodeId === 'string') { var newContentEl = newDom.getElementById(section.nodeId); if (!newContentEl) { return; } document.getElementById(section.nodeId).innerHTML = newContentEl.innerHTML; } else { section.nodeId.forEach(id => { document.getElementById(id).innerHTML = newDom.getElementById(id).innerHTML; }); } } return section; }, getUpdatedParams: function (currentParams, newParams) { const clone = new URLSearchParams(currentParams); const preservedParams = ['sort_by', 'q', 'options[prefix]', 'type']; // Find what params need to be removed // delete happens first as we cannot specify keys based off of values for (const [key, value] of clone.entries()) { if (!newParams.getAll(key).includes(value) && !preservedParams.includes(key)) { clone.delete(key); }; } // Find what params need to be added for (const [key, value] of newParams.entries()) { if (!clone.getAll(key).includes(value) && value !== '') { clone.append(key, value); } } return clone; }, updateURLHash: function (searchParams) { history.pushState( {}, '', `${window.location.pathname}${ searchParams && '?'.concat(searchParams) }`, ); }, log: function (...args) { if (this.debug) { console.log(...args); } }, }); return AjaxRenderer; })(); if (window.Shopify && window.Shopify.theme && navigator && navigator.sendBeacon && window.Shopify.designMode) { navigator.sendBeacon('https://api.archetypethemes.co/api/beacon', new URLSearchParams({ shop: window.Shopify.shop, themeName: window.theme && window.theme.settings && `${window.theme.settings.themeName} v${window.theme.settings.themeVersion}`, role: window.Shopify.theme.role, route: window.location.pathname, themeId: window.Shopify.theme.id, themeStoreId: window.Shopify.theme.theme_store_id || 0, isThemeEditor: !!window.Shopify.designMode })) } theme.cart = { getCart: function() { var url = ''.concat(theme.routes.cart, '?t=').concat(Date.now()); return fetch(url, { credentials: 'same-origin', method: 'GET' }).then(response => response.json()); }, getCartProductMarkup: function() { var url = ''.concat(theme.routes.cartPage, '?t=').concat(Date.now()); url = url.indexOf('?') === -1 ? (url + '?view=ajax') : (url + '&view=ajax'); return fetch(url, { credentials: 'same-origin', method: 'GET' }) .then(function(response) {return response.text()}); }, changeItem: function(key, qty) { return this._updateCart({ url: ''.concat(theme.routes.cartChange, '?t=').concat(Date.now()), data: JSON.stringify({ id: key, quantity: qty }) }) }, _updateCart: function(params) { return fetch(params.url, { method: 'POST', body: params.data, credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(function(cart) { return cart; }); }, updateAttribute: function(key, value) { return this._updateCart({ url: '/cart/update.js', data: JSON.stringify({ attributes: { [key]: theme.cart.attributeToString(value) } }) }); }, updateNote: function(note) { return this._updateCart({ url: '/cart/update.js', data: JSON.stringify({ note: theme.cart.attributeToString(note) }) }); }, attributeToString: function(attribute) { if ((typeof attribute) !== 'string') { attribute += ''; if (attribute === 'undefined') { attribute = ''; } } return attribute.trim(); } } /*============================================================================ CartForm - Prevent checkout when terms checkbox exists - Listen to quantity changes, rebuild cart (both widget and page) ==============================================================================*/ theme.CartForm = (function() { var selectors = { products: '[data-products]', qtySelector: '.js-qty__wrapper', discounts: '[data-discounts]', savings: '[data-savings]', subTotal: '[data-subtotal]', cartBubble: '.cart-link__bubble', cartNote: '[name="note"]', termsCheckbox: '.cart__terms-checkbox', checkoutBtn: '.cart__checkout' }; var classes = { btnLoading: 'btn--loading' }; var config = { requiresTerms: false }; function CartForm(form) { if (!form) { return; } this.form = form; this.wrapper = form.parentNode; this.location = form.dataset.location; this.namespace = '.cart-' + this.location; this.products = form.querySelector(selectors.products) this.submitBtn = form.querySelector(selectors.checkoutBtn); this.discounts = form.querySelector(selectors.discounts); this.savings = form.querySelector(selectors.savings); this.subtotal = form.querySelector(selectors.subTotal); this.termsCheckbox = form.querySelector(selectors.termsCheckbox); this.noteInput = form.querySelector(selectors.cartNote); this.cartItemsUpdated = false; if (this.termsCheckbox) { config.requiresTerms = true; } this.init(); } CartForm.prototype = Object.assign({}, CartForm.prototype, { init: function() { this.initQtySelectors(); document.addEventListener('cart:quantity' + this.namespace, this.quantityChanged.bind(this)); this.form.on('submit' + this.namespace, this.onSubmit.bind(this)); if (this.noteInput) { this.noteInput.addEventListener('change', function() { var newNote = this.value; theme.cart.updateNote(newNote); }); } // Dev-friendly way to build the cart document.addEventListener('cart:build', function() { this.buildCart(); }.bind(this)); }, reInit: function() { this.initQtySelectors(); }, onSubmit: function(evt) { this.submitBtn.classList.add(classes.btnLoading); /* Checks for drawer or cart open class on body element and then stops the form from being submitted. We are also checking against a custom property, this.cartItemsUpdated = false. Error is handled in the quantityChanged method For Expanse/Fetch quick add, if an error is present it is alerted through the add to cart fetch request in quick-add.js. Custom property this.cartItemsUpdated = false is reset in cart-drawer.js for Expanse/Fetch when using quick add */ if (document.documentElement.classList.contains('js-drawer-open') && this.cartItemsUpdated || document.documentElement.classList.contains('cart-open') && this.cartItemsUpdated) { this.submitBtn.classList.remove(classes.btnLoading); evt.preventDefault(); return false; } if (config.requiresTerms) { if (this.termsCheckbox.checked) { // continue to checkout } else { alert(theme.strings.cartTermsConfirmation); this.submitBtn.classList.remove(classes.btnLoading) evt.preventDefault(); return false; } } }, /*============================================================================ Query cart page to get markup ==============================================================================*/ _parseProductHTML: function(html) { var parser = new DOMParser(); var doc = parser.parseFromString(html, 'text/html'); return { items: doc.querySelector('.cart__items'), discounts: doc.querySelector('.cart__discounts') } }, buildCart: function() { theme.cart.getCartProductMarkup().then(this.cartMarkup.bind(this)); }, cartMarkup: function(html) { var markup = this._parseProductHTML(html); var items = markup.items; var count = parseInt(items.dataset.count); var subtotal = items.dataset.cartSubtotal; var savings = items.dataset.cartSavings; this.updateCartDiscounts(markup.discounts); this.updateSavings(savings); if (count > 0) { this.wrapper.classList.remove('is-empty'); } else { this.wrapper.classList.add('is-empty'); } this.updateCount(count); // Append item markup this.products.innerHTML = ''; this.products.append(items); /* When images load in Safari they have no intrinsic size so we are forcing the images to be parsed again. Instead of checking for the Safari browser we can add a check for a 0 natural width and 0 natural height for the first image */ const firstImage = this.products.querySelector('.cart__image img') || this.products.querySelector('.cart__image svg'); if (firstImage && firstImage.naturalWidth === 0 && firstImage.naturalHeight === 0) { this.products.querySelectorAll('img').forEach(image => image.outerHTML = image.outerHTML); } // Update subtotal this.subtotal.innerHTML = theme.Currency.formatMoney(subtotal, theme.settings.moneyFormat); this.reInit(); if (window.AOS) { AOS.refreshHard() } if (Shopify && Shopify.StorefrontExpressButtons) { Shopify.StorefrontExpressButtons.initialize(); } }, updateCartDiscounts: function(markup) { if (!this.discounts) { return; } this.discounts.innerHTML = ''; this.discounts.append(markup); }, /*============================================================================ Quantity handling ==============================================================================*/ initQtySelectors: function() { this.form.querySelectorAll(selectors.qtySelector).forEach(el => { var selector = new theme.QtySelector(el, { namespace: this.namespace, isCart: true }); }); }, quantityChanged: function(evt) { var key = evt.detail[0]; var qty = evt.detail[1]; var el = evt.detail[2]; if (!key || !qty) { return; } // Disable qty selector so multiple clicks can't happen while loading if (el) { el.classList.add('is-loading'); } // Update cartItemsUpdated property on object so we can reference later if (evt.type === 'cart:quantity.cart-cart-drawer' || evt.type === 'cart:quantity.cart-header') { this.cartItemsUpdated = true; } theme.cart.changeItem(key, qty) .then(function(cart) { const updatedItem = cart.items.find(item => item.key === key); if ((updatedItem && evt.type === 'cart:quantity.cart-cart-drawer') || (updatedItem && evt.type === 'cart:quantity.cart-header')) { if (updatedItem.quantity !== qty) { alert(theme.strings.maxQuantity.replace('[quantity]', updatedItem.quantity).replace('[title]', updatedItem.title)); } // Reset property on object so that checkout button will work as usual this.cartItemsUpdated = false; } if (cart.item_count > 0) { this.wrapper.classList.remove('is-empty'); } else { this.wrapper.classList.add('is-empty'); } this.buildCart(); document.dispatchEvent(new CustomEvent('cart:updated', { detail: { cart: cart } })); }.bind(this)) .catch(function(XMLHttpRequest){}); }, /*============================================================================ Update elements of the cart ==============================================================================*/ updateSubtotal: function(subtotal) { this.form.querySelector(selectors.subTotal).innerHTML = theme.Currency.formatMoney(subtotal, theme.settings.moneyFormat); }, updateSavings: function(savings) { if (!this.savings) { return; } if (savings > 0) { var amount = theme.Currency.formatMoney(savings, theme.settings.moneyFormat); this.savings.classList.remove('hide'); this.savings.innerHTML = theme.strings.cartSavings.replace('[savings]', amount); } else { this.savings.classList.add('hide'); } }, updateCount: function(count) { var countEls = document.querySelectorAll('.cart-link__bubble-num'); if (countEls.length) { countEls.forEach(el => { el.innerText = count; }); } // show/hide bubble(s) var bubbles = document.querySelectorAll(selectors.cartBubble); if (bubbles.length) { if (count > 0) { bubbles.forEach(b => { b.classList.add('cart-link__bubble--visible'); }); } else { bubbles.forEach(b => { b.classList.remove('cart-link__bubble--visible'); }); } } } }); return CartForm; })(); // Either collapsible containers all acting individually, // or tabs that can only have one open at a time theme.collapsibles = (function() { var selectors = { trigger: '.collapsible-trigger', module: '.collapsible-content', moduleInner: '.collapsible-content__inner', tabs: '.collapsible-trigger--tab' }; var classes = { hide: 'hide', open: 'is-open', autoHeight: 'collapsible--auto-height', tabs: 'collapsible-trigger--tab' }; var namespace = '.collapsible'; var isTransitioning = false; function init(scope) { var el = scope ? scope : document; el.querySelectorAll(selectors.trigger).forEach(trigger => { var state = trigger.classList.contains(classes.open); trigger.setAttribute('aria-expanded', state); trigger.off('click' + namespace); trigger.on('click' + namespace, toggle); }); } function toggle(evt) { if (isTransitioning) { return; } isTransitioning = true; var el = evt.currentTarget; var isOpen = el.classList.contains(classes.open); var isTab = el.classList.contains(classes.tabs); var moduleId = el.getAttribute('aria-controls'); var container = document.getElementById(moduleId); if (!moduleId) { moduleId = el.dataset.controls; } // No ID, bail if (!moduleId) { return; } // If container=null, there isn't a matching ID. // Check if data-id is set instead. Could be multiple. // Select based on being in the same parent div. if (!container) { var multipleMatches = document.querySelectorAll('[data-id="' + moduleId + '"]'); if (multipleMatches.length > 0) { container = el.parentNode.querySelector('[data-id="' + moduleId + '"]'); } } if (!container) { isTransitioning = false; return; } var height = container.querySelector(selectors.moduleInner).offsetHeight; var isAutoHeight = container.classList.contains(classes.autoHeight); var parentCollapsibleEl = container.parentNode.closest(selectors.module); var childHeight = height; if (isTab) { if(isOpen) { isTransitioning = false; return; } var newModule; document.querySelectorAll(selectors.tabs + '[data-id="'+ el.dataset.id +'"]').forEach(el => { el.classList.remove(classes.open); newModule = document.querySelector('#' + el.getAttribute('aria-controls')); setTransitionHeight(newModule, 0, true); }); } // If isAutoHeight, set the height to 0 just after setting the actual height // so the closing animation works nicely if (isOpen && isAutoHeight) { setTimeout(function() { height = 0; setTransitionHeight(container, height, isOpen, isAutoHeight); }, 0); } if (isOpen && !isAutoHeight) { height = 0; } el.setAttribute('aria-expanded', !isOpen); if (isOpen) { el.classList.remove(classes.open); } else { el.classList.add(classes.open); } setTransitionHeight(container, height, isOpen, isAutoHeight); // If we are in a nested collapsible element like the mobile nav, // also set the parent element's height if (parentCollapsibleEl) { var parentHeight = parentCollapsibleEl.style.height; if (isOpen && parentHeight === 'auto') { childHeight = 0; // Set childHeight to 0 if parent is initially opened } var totalHeight = isOpen ? parentCollapsibleEl.offsetHeight - childHeight : height + parentCollapsibleEl.offsetHeight; setTransitionHeight(parentCollapsibleEl, totalHeight, false, false); } // If Shopify Product Reviews app installed, // resize container on 'Write review' click // that shows form if (window.SPR) { var btn = container.querySelector('.spr-summary-actions-newreview'); if (!btn) { return } btn.off('click' + namespace); btn.on('click' + namespace, function() { height = container.querySelector(selectors.moduleInner).offsetHeight; setTransitionHeight(container, height, isOpen, isAutoHeight); }); } } function setTransitionHeight(container, height, isOpen, isAutoHeight) { container.classList.remove(classes.hide); theme.utils.prepareTransition(container, function() { container.style.height = height+'px'; if (isOpen) { container.classList.remove(classes.open); } else { container.classList.add(classes.open); } }); if (!isOpen && isAutoHeight) { var o = container; window.setTimeout(function() { o.css('height','auto'); isTransitioning = false; }, 500); } else { isTransitioning = false; } } return { init: init }; })(); // Shopify-built select-like popovers for currency and language selection theme.Disclosure = (function() { var selectors = { disclosureForm: '[data-disclosure-form]', disclosureList: '[data-disclosure-list]', disclosureToggle: '[data-disclosure-toggle]', disclosureInput: '[data-disclosure-input]', disclosureOptions: '[data-disclosure-option]' }; var classes = { listVisible: 'disclosure-list--visible' }; function Disclosure(disclosure) { this.container = disclosure; this._cacheSelectors(); this._setupListeners(); } Disclosure.prototype = Object.assign({}, Disclosure.prototype, { _cacheSelectors: function() { this.cache = { disclosureForm: this.container.closest(selectors.disclosureForm), disclosureList: this.container.querySelector(selectors.disclosureList), disclosureToggle: this.container.querySelector( selectors.disclosureToggle ), disclosureInput: this.container.querySelector( selectors.disclosureInput ), disclosureOptions: this.container.querySelectorAll( selectors.disclosureOptions ) }; }, _setupListeners: function() { this.eventHandlers = this._setupEventHandlers(); this.cache.disclosureToggle.addEventListener( 'click', this.eventHandlers.toggleList ); this.cache.disclosureOptions.forEach(function(disclosureOption) { disclosureOption.addEventListener( 'click', this.eventHandlers.connectOptions ); }, this); this.container.addEventListener( 'keyup', this.eventHandlers.onDisclosureKeyUp ); this.cache.disclosureList.addEventListener( 'focusout', this.eventHandlers.onDisclosureListFocusOut ); this.cache.disclosureToggle.addEventListener( 'focusout', this.eventHandlers.onDisclosureToggleFocusOut ); document.body.addEventListener('click', this.eventHandlers.onBodyClick); }, _setupEventHandlers: function() { return { connectOptions: this._connectOptions.bind(this), toggleList: this._toggleList.bind(this), onBodyClick: this._onBodyClick.bind(this), onDisclosureKeyUp: this._onDisclosureKeyUp.bind(this), onDisclosureListFocusOut: this._onDisclosureListFocusOut.bind(this), onDisclosureToggleFocusOut: this._onDisclosureToggleFocusOut.bind(this) }; }, _connectOptions: function(event) { event.preventDefault(); this._submitForm(event.currentTarget.dataset.value); }, _onDisclosureToggleFocusOut: function(event) { var disclosureLostFocus = this.container.contains(event.relatedTarget) === false; if (disclosureLostFocus) { this._hideList(); } }, _onDisclosureListFocusOut: function(event) { var childInFocus = event.currentTarget.contains(event.relatedTarget); var isVisible = this.cache.disclosureList.classList.contains( classes.listVisible ); if (isVisible && !childInFocus) { this._hideList(); } }, _onDisclosureKeyUp: function(event) { if (event.which !== 27) return; this._hideList(); this.cache.disclosureToggle.focus(); }, _onBodyClick: function(event) { var isOption = this.container.contains(event.target); var isVisible = this.cache.disclosureList.classList.contains( classes.listVisible ); if (isVisible && !isOption) { this._hideList(); } }, _submitForm: function(value) { this.cache.disclosureInput.value = value; this.cache.disclosureForm.submit(); }, _hideList: function() { this.cache.disclosureList.classList.remove(classes.listVisible); this.cache.disclosureToggle.setAttribute('aria-expanded', false); }, _toggleList: function() { var ariaExpanded = this.cache.disclosureToggle.getAttribute('aria-expanded') === 'true'; this.cache.disclosureList.classList.toggle(classes.listVisible); this.cache.disclosureToggle.setAttribute('aria-expanded', !ariaExpanded); }, destroy: function() { this.cache.disclosureToggle.removeEventListener( 'click', this.eventHandlers.toggleList ); this.cache.disclosureOptions.forEach(function(disclosureOption) { disclosureOption.removeEventListener( 'click', this.eventHandlers.connectOptions ); }, this); this.container.removeEventListener( 'keyup', this.eventHandlers.onDisclosureKeyUp ); this.cache.disclosureList.removeEventListener( 'focusout', this.eventHandlers.onDisclosureListFocusOut ); this.cache.disclosureToggle.removeEventListener( 'focusout', this.eventHandlers.onDisclosureToggleFocusOut ); document.body.removeEventListener( 'click', this.eventHandlers.onBodyClick ); } }); return Disclosure; })(); theme.Drawers = (function() { function Drawers(id, name) { this.config = { id: id, close: '.js-drawer-close', open: '.js-drawer-open-' + name, openClass: 'js-drawer-open', closingClass: 'js-drawer-closing', activeDrawer: 'drawer--is-open', namespace: '.drawer-' + name }; this.nodes = { page: document.querySelector('#MainContent') }; this.drawer = document.querySelector('#' + id); this.isOpen = false; if (!this.drawer) { return; } this.init(); } Drawers.prototype = Object.assign({}, Drawers.prototype, { init: function() { // Setup open button(s) document.querySelectorAll(this.config.open).forEach(openBtn => { openBtn.setAttribute('aria-expanded', 'false'); openBtn.addEventListener('click', this.open.bind(this)); }); this.drawer.querySelector(this.config.close).addEventListener('click', this.close.bind(this)); // Close modal if a drawer is opened document.addEventListener('modalOpen', function() { this.close(); }.bind(this)); }, open: function(evt, returnFocusEl) { if (evt) { evt.preventDefault(); } if (this.isOpen) { return; } // Without this the drawer opens, the click event bubbles up to $nodes.page which closes the drawer. if (evt && evt.stopPropagation) { evt.stopPropagation(); // save the source of the click, we'll focus to this on close evt.currentTarget.setAttribute('aria-expanded', 'true'); this.activeSource = evt.currentTarget; } else if (returnFocusEl) { returnFocusEl.setAttribute('aria-expanded', 'true'); this.activeSource = returnFocusEl; } theme.utils.prepareTransition(this.drawer, function() { this.drawer.classList.add(this.config.activeDrawer); }.bind(this)); document.documentElement.classList.add(this.config.openClass); this.isOpen = true; theme.a11y.trapFocus({ container: this.drawer, namespace: 'drawer_focus' }); document.dispatchEvent(new CustomEvent('drawerOpen')); document.dispatchEvent(new CustomEvent('drawerOpen.' + this.config.id)); this.bindEvents(); }, close: function(evt) { if (!this.isOpen) { return; } // Do not close if click event came from inside drawer if (evt) { if (evt.target.closest('.js-drawer-close')) { // Do not close if using the drawer close button } else if (evt.target.closest('.drawer')) { return; } } // deselect any focused form elements document.activeElement.blur(); theme.utils.prepareTransition(this.drawer, function() { this.drawer.classList.remove(this.config.activeDrawer); }.bind(this)); document.documentElement.classList.remove(this.config.openClass); document.documentElement.classList.add(this.config.closingClass); window.setTimeout(function() { document.documentElement.classList.remove(this.config.closingClass); if (this.activeSource && this.activeSource.getAttribute('aria-expanded')) { this.activeSource.setAttribute('aria-expanded', 'false'); this.activeSource.focus(); } }.bind(this), 500); this.isOpen = false; theme.a11y.removeTrapFocus({ container: this.drawer, namespace: 'drawer_focus' }); this.unbindEvents(); }, bindEvents: function() { // Clicking out of drawer closes it window.on('click' + this.config.namespace, function(evt) { this.close(evt) return; }.bind(this)); // Pressing escape closes drawer window.on('keyup' + this.config.namespace, function(evt) { if (evt.keyCode === 27) { this.close(); } }.bind(this)); theme.a11y.lockMobileScrolling(this.config.namespace, this.nodes.page); }, unbindEvents: function() { window.off('click' + this.config.namespace); window.off('keyup' + this.config.namespace); theme.a11y.unlockMobileScrolling(this.config.namespace, this.nodes.page); } }); return Drawers; })(); theme.Modals = (function() { function Modal(id, name, options) { var defaults = { close: '.js-modal-close', open: '.js-modal-open-' + name, openClass: 'modal--is-active', closingClass: 'modal--is-closing', bodyOpenClass: ['modal-open'], bodyOpenSolidClass: 'modal-open--solid', bodyClosingClass: 'modal-closing', closeOffContentClick: true }; this.id = id; this.modal = document.getElementById(id); if (!this.modal) { return false; } this.modalContent = this.modal.querySelector('.modal__inner'); this.config = Object.assign(defaults, options); this.modalIsOpen = false; this.focusOnOpen = this.config.focusIdOnOpen ? document.getElementById(this.config.focusIdOnOpen) : this.modal; this.isSolid = this.config.solid; this.init(); } Modal.prototype.init = function() { document.querySelectorAll(this.config.open).forEach(btn => { btn.setAttribute('aria-expanded', 'false'); btn.addEventListener('click', this.open.bind(this)); }); this.modal.querySelectorAll(this.config.close).forEach(btn => { btn.addEventListener('click', this.close.bind(this)); }); // Close modal if a drawer is opened document.addEventListener('drawerOpen', function() { this.close(); }.bind(this)); }; Modal.prototype.open = function(evt) { // Keep track if modal was opened from a click, or called by another function var externalCall = false; // don't open an opened modal if (this.modalIsOpen) { return; } // Prevent following href if link is clicked if (evt) { evt.preventDefault(); } else { externalCall = true; } // Without this, the modal opens, the click event bubbles up to $nodes.page // which closes the modal. if (evt && evt.stopPropagation) { evt.stopPropagation(); // save the source of the click, we'll focus to this on close this.activeSource = evt.currentTarget.setAttribute('aria-expanded', 'true'); } if (this.modalIsOpen && !externalCall) { this.close(); } this.modal.classList.add(this.config.openClass); document.documentElement.classList.add(...this.config.bodyOpenClass); if (this.isSolid) { document.documentElement.classList.add(this.config.bodyOpenSolidClass); } this.modalIsOpen = true; theme.a11y.trapFocus({ container: this.modal, elementToFocus: this.focusOnOpen, namespace: 'modal_focus' }); document.dispatchEvent(new CustomEvent('modalOpen')); document.dispatchEvent(new CustomEvent('modalOpen.' + this.id)); this.bindEvents(); }; Modal.prototype.close = function(evt) { // don't close a closed modal if (!this.modalIsOpen) { return; } // Do not close modal if click happens inside modal content if (evt) { if (evt.target.closest('.js-modal-close')) { // Do not close if using the modal close button } else if (evt.target.closest('.modal__inner')) { return; } } // deselect any focused form elements document.activeElement.blur(); this.modal.classList.remove(this.config.openClass); this.modal.classList.add(this.config.closingClass); document.documentElement.classList.remove(...this.config.bodyOpenClass); document.documentElement.classList.add(this.config.bodyClosingClass); window.setTimeout(function() { document.documentElement.classList.remove(this.config.bodyClosingClass); this.modal.classList.remove(this.config.closingClass); if (this.activeSource && this.activeSource.getAttribute('aria-expanded')) { this.activeSource.setAttribute('aria-expanded', 'false').focus(); } }.bind(this), 500); // modal close css transition if (this.isSolid) { document.documentElement.classList.remove(this.config.bodyOpenSolidClass); } this.modalIsOpen = false; theme.a11y.removeTrapFocus({ container: this.modal, namespace: 'modal_focus' }); document.dispatchEvent(new CustomEvent('modalClose.' + this.id)); this.unbindEvents(); }; Modal.prototype.bindEvents = function() { window.on('keyup.modal', function(evt) { if (evt.keyCode === 27) { this.close(); } }.bind(this)); if (this.config.closeOffContentClick) { // Clicking outside of the modal content also closes it this.modal.on('click.modal', this.close.bind(this)); } }; Modal.prototype.unbindEvents = function() { document.documentElement.off('.modal'); if (this.config.closeOffContentClick) { this.modal.off('.modal'); } }; return Modal; })(); /*============================================================================ ParallaxImage ==============================================================================*/ class ParallaxImage extends HTMLElement { constructor() { super(); this.parallaxImage = this.querySelector('[data-parallax-image]'); this.windowInnerHeight = window.innerHeight; this.isActive = false; this.timeout = null; this.directionMap = { right: 0, top: 90, left: 180, bottom: 270 } this.directionMultipliers = { 0: [ 1, 0 ], 90: [ 0, -1 ], 180: [ -1, 0 ], 270: [ 0, 1 ] } this.init(); window.addEventListener('scroll', () => this.scrollHandler()); } getParallaxInfo() { const { width, height, top } = this.parallaxImage.getBoundingClientRect(); let element = this.parallaxImage; let multipliers; let { angle, movement } = element.dataset; let movementPixels = angle === 'top' ? Math.ceil(height * (parseFloat(movement) / 100)) : Math.ceil(width * (parseFloat(movement) / 100)); // angle has shorthands "top", "left", "bottom" and "right" // nullish coalescing. using `||` here would fail for `0` angle = this.directionMap[angle] ?? parseFloat(angle); // fallback if undefined // NaN is the only value that doesn't equal itself if (angle !== angle) angle = 270; // move to bottom (default parallax effect) if (movementPixels !== movementPixels) movementPixels = 100; // 100px // check if angle is located in top half and/or left half angle %= 360; if (angle < 0) angle += 360 const toLeft = angle > 90 && angle < 270; const toTop = angle < 180; element.style[toLeft ? 'left' : 'right'] = 0; element.style[toTop ? 'top' : 'bottom'] = 0; // if it's not a perfectly horizontal or vertical movement, get cos and sin if (angle % 90) { const radians = angle * Math.PI / 180 multipliers = [ Math.cos(radians), Math.sin(radians) * -1 ] // only sin has to be inverted } else { multipliers = this.directionMultipliers[angle]; } // increase width and height according to movement and multipliers if (multipliers[0]) element.style.width = `calc(100% + ${movementPixels * Math.abs(multipliers[0])}px)`; if (multipliers[1]) element.style.height = `calc(100% + ${movementPixels * Math.abs(multipliers[1])}px)`; return { element, movementPixels, multipliers, top, height } } init() { const { element, movementPixels, multipliers, top, height } = this.getParallaxInfo();; const scrolledInContainer = this.windowInnerHeight - top; const scrollArea = this.windowInnerHeight + height; const progress = scrolledInContainer / scrollArea; if (progress > -0.1 && progress < 1.1) { const position = Math.min(Math.max(progress, 0), 1) * movementPixels; element.style.transform = `translate3d(${position * multipliers[0]}px, ${position * multipliers[1]}px, 0)`; } if (this.isActive) requestAnimationFrame(this.init.bind(this)); } scrollHandler() { if (this.isActive) { clearTimeout(this.timeout); } else { this.isActive = true; requestAnimationFrame(this.init.bind(this)); } this.timeout = setTimeout(() => this.isActive = false, 20); } } customElements.define('parallax-image', ParallaxImage); if (typeof window.noUiSlider === 'undefined') { throw new Error('theme.PriceRange is missing vendor noUiSlider: // =require vendor/nouislider.js'); } theme.PriceRange = (function () { var defaultStep = 10; var selectors = { priceRange: '.price-range', priceRangeSlider: '.price-range__slider', priceRangeInputMin: '.price-range__input-min', priceRangeInputMax: '.price-range__input-max', priceRangeDisplayMin: '.price-range__display-min', priceRangeDisplayMax: '.price-range__display-max', }; function PriceRange(container, {onChange, onUpdate, ...sliderOptions} = {}) { this.container = container; this.onChange = onChange; this.onUpdate = onUpdate; this.sliderOptions = sliderOptions || {}; return this.init(); } PriceRange.prototype = Object.assign({}, PriceRange.prototype, { init: function () { if (!this.container.classList.contains('price-range')) { throw new Error('You must instantiate PriceRange with a valid container') } this.formEl = this.container.closest('form'); this.sliderEl = this.container.querySelector(selectors.priceRangeSlider); this.inputMinEl = this.container.querySelector(selectors.priceRangeInputMin); this.inputMaxEl = this.container.querySelector(selectors.priceRangeInputMax); this.displayMinEl = this.container.querySelector(selectors.priceRangeDisplayMin); this.displayMaxEl = this.container.querySelector(selectors.priceRangeDisplayMax); this.minRange = parseFloat(this.container.dataset.min) || 0; this.minValue = parseFloat(this.container.dataset.minValue) || 0; this.maxRange = parseFloat(this.container.dataset.max) || 100; this.maxValue = parseFloat(this.container.dataset.maxValue) || this.maxRange; return this.createPriceRange(); }, createPriceRange: function () { if (this.sliderEl && this.sliderEl.noUiSlider && typeof this.sliderEl.noUiSlider.destroy === 'function') { this.sliderEl.noUiSlider.destroy(); } var slider = noUiSlider.create(this.sliderEl, { connect: true, step: defaultStep, ...this.sliderOptions, // Do not allow overriding these options start: [this.minValue, this.maxValue], range: { min: this.minRange, max: this.maxRange, }, }); slider.on('update', values => { this.displayMinEl.innerHTML = theme.Currency.formatMoney( values[0], theme.settings.moneyFormat, ); this.displayMaxEl.innerHTML = theme.Currency.formatMoney( values[1], theme.settings.moneyFormat, ); if (this.onUpdate) { this.onUpdate(values); } }); slider.on('change', values => { this.inputMinEl.value = values[0]; this.inputMaxEl.value = values[1]; if (this.onChange) { const formData = new FormData(this.formEl); this.onChange(formData); } }); return slider; }, }); return PriceRange; })(); theme.AjaxProduct = (function() { var status = { loading: false }; function ProductForm(form, submit, args) { this.form = form; this.args = args; var submitSelector = submit ? submit : '.add-to-cart'; if (this.form) { this.addToCart = form.querySelector(submitSelector); this.form.addEventListener('submit', this.addItemFromForm.bind(this)); } }; ProductForm.prototype = Object.assign({}, ProductForm.prototype, { addItemFromForm: function(evt, callback){ evt.preventDefault(); if (status.loading) { return; } // Loading indicator on add to cart button this.addToCart.classList.add('btn--loading'); status.loading = true; var data = theme.utils.serialize(this.form); fetch(theme.routes.cartAdd, { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(function(data) { if (data.status === 422) { this.error(data); } else { var product = data; this.success(product); } status.loading = false; this.addToCart.classList.remove('btn--loading'); // Reload page if adding product from a section on the cart page if (document.body.classList.contains('template-cart')) { window.scrollTo(0, 0); location.reload(); } }.bind(this)); }, success: function(product) { var errors = this.form.querySelector('.errors'); if (errors) { errors.remove(); } document.dispatchEvent(new CustomEvent('ajaxProduct:added', { detail: { product: product, addToCartBtn: this.addToCart } })); if (this.args && this.args.scopedEventId) { document.dispatchEvent(new CustomEvent('ajaxProduct:added:' + this.args.scopedEventId, { detail: { product: product, addToCartBtn: this.addToCart } })); } }, error: function(error) { if (!error.description) { console.warn(error); return; } var errors = this.form.querySelector('.errors'); if (errors) { errors.remove(); } var errorDiv = document.createElement('div'); errorDiv.classList.add('errors', 'text-center'); errorDiv.textContent = error.description; this.form.append(errorDiv); document.dispatchEvent(new CustomEvent('ajaxProduct:error', { detail: { errorMessage: error.description } })); if (this.args && this.args.scopedEventId) { document.dispatchEvent(new CustomEvent('ajaxProduct:error:' + this.args.scopedEventId, { detail: { errorMessage: error.description } })); } } }); return ProductForm; })(); theme.ProductMedia = (function() { var modelJsonSections = {}; var models = {}; var xrButtons = {}; var selectors = { mediaGroup: '[data-product-single-media-group]', xrButton: '[data-shopify-xr]' }; function init(modelViewerContainers, sectionId) { modelJsonSections[sectionId] = { loaded: false }; modelViewerContainers.forEach(function(container, index) { var mediaId = container.dataset.mediaId; var modelViewerElement = container.querySelector('model-viewer'); var modelId = modelViewerElement.dataset.modelId; if (index === 0) { var mediaGroup = container.closest(selectors.mediaGroup); var xrButton = mediaGroup.querySelector(selectors.xrButton); xrButtons[sectionId] = { element: xrButton, defaultId: modelId }; } models[mediaId] = { modelId: modelId, sectionId: sectionId, container: container, element: modelViewerElement }; }); window.Shopify.loadFeatures([ { name: 'shopify-xr', version: '1.0', onLoad: setupShopifyXr }, { name: 'model-viewer-ui', version: '1.0', onLoad: setupModelViewerUi } ]); theme.LibraryLoader.load('modelViewerUiStyles'); } function setupShopifyXr(errors) { if (errors) return; if (!window.ShopifyXR) { document.addEventListener('shopify_xr_initialized', function() { setupShopifyXr(); }); return; } for (var sectionId in modelJsonSections) { if (modelJsonSections.hasOwnProperty(sectionId)) { var modelSection = modelJsonSections[sectionId]; if (modelSection.loaded) continue; var modelJson = document.querySelector('#ModelJson-' + sectionId); window.ShopifyXR.addModels(JSON.parse(modelJson.innerHTML)); modelSection.loaded = true; } } window.ShopifyXR.setupXRElements(); } function setupModelViewerUi(errors) { if (errors) return; for (var key in models) { if (models.hasOwnProperty(key)) { var model = models[key]; if (!model.modelViewerUi && Shopify) { model.modelViewerUi = new Shopify.ModelViewerUI(model.element); } setupModelViewerListeners(model); } } } function setupModelViewerListeners(model) { var xrButton = xrButtons[model.sectionId]; model.container.addEventListener('mediaVisible', function() { xrButton.element.setAttribute('data-shopify-model3d-id', model.modelId); if (theme.config.isTouch) return; model.modelViewerUi.play(); }); model.container.addEventListener('mediaHidden', function() { xrButton.element.setAttribute('data-shopify-model3d-id', xrButton.defaultId); model.modelViewerUi.pause(); }); model.container.addEventListener('xrLaunch', function() { model.modelViewerUi.pause(); }); } function removeSectionModels(sectionId) { for (var key in models) { if (models.hasOwnProperty(key)) { var model = models[key]; if (model.sectionId === sectionId) { delete models[key]; } } } delete modelJsonSections[sectionId]; } return { init: init, removeSectionModels: removeSectionModels }; })(); theme.QtySelector = (function() { var selectors = { input: '.js-qty__num', plus: '.js-qty__adjust--plus', minus: '.js-qty__adjust--minus' }; function QtySelector(el, options) { this.wrapper = el; this.plus = el.querySelector(selectors.plus); this.minus = el.querySelector(selectors.minus); this.input = el.querySelector(selectors.input); this.minValue = this.input.getAttribute('min') || 1; var defaults = { namespace: null, isCart: false, key: this.input.dataset.id }; this.options = Object.assign({}, defaults, options); this.init(); } QtySelector.prototype = Object.assign({}, QtySelector.prototype, { init: function() { this.plus.addEventListener('click', function() { var qty = this._getQty(); this._change(qty + 1); }.bind(this)); this.minus.addEventListener('click', function() { var qty = this._getQty(); this._change(qty - 1); }.bind(this)); this.input.addEventListener('change', function(evt) { this._change(this._getQty()); }.bind(this)); }, _getQty: function() { var qty = this.input.value; if((parseFloat(qty) == parseInt(qty)) && !isNaN(qty)) { // We have a valid number! } else { // Not a number. Default to 1. qty = 1; } return parseInt(qty); }, _change: function(qty) { if (qty <= this.minValue) { qty = this.minValue; } this.input.value = qty; if (this.options.isCart) { document.dispatchEvent(new CustomEvent('cart:quantity' + this.options.namespace, { detail: [this.options.key, qty, this.wrapper] })); } } }); return QtySelector; })(); theme.initQuickShop = function() { var ids = []; var products = document.querySelectorAll('.grid-product'); if (!products.length || !theme.settings.quickView) { return; } products.forEach(product => { product.addEventListener('mouseover', productMouseover); }); function productMouseover(evt) { var el = evt.currentTarget; // No quick view on mobile breakpoint if (!theme.config.bpSmall) { el.removeEventListener('mouseover', productMouseover); if (!el || !el.dataset.productId) { // Onboarding product, no real data return; } var productId = el.dataset.productId; var handle = el.dataset.productHandle; var btn = el.querySelector('.quick-product__btn'); theme.preloadProductModal(handle, productId, btn); } } }; theme.preloadProductModal = function(handle, productId, btn) { var holder = document.getElementById('QuickShopHolder-' + handle); var url = theme.routes.home + '/products/' + handle + '?view=modal'; // remove double `/` in case shop might have /en or language in URL url = url.replace('//', '/'); fetch(url).then(function(response) { return response.text(); }).then(function(html) { // Convert the HTML string into a document object var parser = new DOMParser(); var doc = parser.parseFromString(html, 'text/html'); var div = doc.querySelector('.product-section[data-product-handle="'+handle+'"]'); if (!holder) { return; } holder.innerHTML = ''; holder.append(div); // Setup quick view modal var modalId = 'QuickShopModal-' + productId; var name = 'quick-modal-' + productId; new theme.Modals(modalId, name); // Register product template inside quick view theme.sections.register('product', theme.Product, holder); // Register collapsible elements theme.collapsibles.init(); // Register potential video modal links (when video has sound) theme.videoModal(); if (btn) { btn.classList.remove('quick-product__btn--not-ready'); } }); } // theme.Slideshow handles all flickity based sliders // Child navigation is only setup to work on product images theme.Slideshow = (function() { var classes = { animateOut: 'animate-out', isPaused: 'is-paused', isActive: 'is-active' }; var selectors = { allSlides: '.slideshow__slide', currentSlide: '.is-selected', wrapper: '.slideshow-wrapper', pauseButton: '.slideshow__pause' }; var productSelectors = { thumb: '.product__thumb-item:not(.hide)', links: '.product__thumb-item:not(.hide) a', arrow: '.product__thumb-arrow' }; var defaults = { adaptiveHeight: false, autoPlay: false, avoidReflow: false, // custom by Archetype childNav: null, // element. Custom by Archetype instead of asNavFor childNavScroller: null, // element childVertical: false, dragThreshold: 7, fade: false, friction: 0.8, initialIndex: 0, pageDots: false, pauseAutoPlayOnHover: false, prevNextButtons: false, rightToLeft: theme.config.rtl, selectedAttraction: 0.14, setGallerySize: true, wrapAround: true }; function slideshow(el, args) { this.el = el; this.args = Object.assign({}, defaults, args); // Setup listeners as part of arguments this.args.on = { ready: this.init.bind(this), change: this.slideChange.bind(this), settle: this.afterChange.bind(this) }; if (this.args.childNav) { this.childNavEls = this.args.childNav.querySelectorAll(productSelectors.thumb); this.childNavLinks = this.args.childNav.querySelectorAll(productSelectors.links); this.arrows = this.args.childNav.querySelectorAll(productSelectors.arrow); if (this.childNavLinks.length) { this.initChildNav(); } } if (this.args.avoidReflow) { avoidReflow(el); } this.slideshow = new Flickity(el, this.args); // Prevent dragging on the product slider from triggering a zoom on product images if (el.dataset.zoom && el.dataset.zoom === 'true') { this.slideshow.on('dragStart', () => { this.slideshow.slider.style.pointerEvents = 'none'; // With fade enabled, we also need to adjust the pointerEvents on the selected slide if (this.slideshow.options.fade) { this.slideshow.slider.querySelector('.is-selected').style.pointerEvents = 'none'; } }); this.slideshow.on('dragEnd', () => { this.slideshow.slider.style.pointerEvents = 'auto'; // With fade enabled, we also need to adjust the pointerEvents on the selected slide if (this.slideshow.options.fade) { this.slideshow.slider.querySelector('.is-selected').style.pointerEvents = 'auto'; } }); } if (this.args.autoPlay) { var wrapper = el.closest(selectors.wrapper); this.pauseBtn = wrapper.querySelector(selectors.pauseButton); if (this.pauseBtn) { this.pauseBtn.addEventListener('click', this._togglePause.bind(this)); } } // Reset dimensions on resize window.on('resize', theme.utils.debounce(300, function() { this.resize(); }.bind(this))); // Set flickity-viewport height to first element to // avoid awkward page reflows while initializing. // Must be added in a `style` tag because element does not exist yet. // Slideshow element must have an ID function avoidReflow(el) { if (!el.id) return; var firstChild = el.firstChild; while(firstChild != null && firstChild.nodeType == 3){ // skip TextNodes firstChild = firstChild.nextSibling; } var style = document.createElement('style'); style.innerHTML = `#${el.id} .flickity-viewport{height:${firstChild.offsetHeight}px}`; document.head.appendChild(style); } } slideshow.prototype = Object.assign({}, slideshow.prototype, { init: function(el) { this.currentSlide = this.el.querySelector(selectors.currentSlide); // Optional onInit callback if (this.args.callbacks && this.args.callbacks.onInit) { if (typeof this.args.callbacks.onInit === 'function') { this.args.callbacks.onInit(this.currentSlide); } } if (window.AOS) { AOS.refresh() } }, slideChange: function(index) { // Outgoing fade styles if (this.args.fade && this.currentSlide) { this.currentSlide.classList.add(classes.animateOut); this.currentSlide.addEventListener('transitionend', function() { this.currentSlide.classList.remove(classes.animateOut); }.bind(this)); } // Match index with child nav if (this.args.childNav) { this.childNavGoTo(index); } // Optional onChange callback if (this.args.callbacks && this.args.callbacks.onChange) { if (typeof this.args.callbacks.onChange === 'function') { this.args.callbacks.onChange(index); } } // Show/hide arrows depending on selected index if (this.arrows && this.arrows.length) { this.arrows[0].classList.toggle('hide', index === 0); this.arrows[1].classList.toggle('hide', index === (this.childNavLinks.length - 1)); } }, afterChange: function(index) { // Remove all fade animation classes after slide is done if (this.args.fade) { this.el.querySelectorAll(selectors.allSlides).forEach(slide => { slide.classList.remove(classes.animateOut); }); } this.currentSlide = this.el.querySelector(selectors.currentSlide); // Match index with child nav (in case slider height changed first) if (this.args.childNav) { this.childNavGoTo(this.slideshow.selectedIndex); } }, destroy: function() { if (this.args.childNav && this.childNavLinks.length) { this.childNavLinks.forEach(a => { a.classList.remove(classes.isActive); }); } this.slideshow.destroy(); }, reposition: function() { this.slideshow.reposition(); }, _togglePause: function() { if (this.pauseBtn.classList.contains(classes.isPaused)) { this.pauseBtn.classList.remove(classes.isPaused); this.slideshow.playPlayer(); } else { this.pauseBtn.classList.add(classes.isPaused); this.slideshow.pausePlayer(); } }, resize: function() { this.slideshow.resize(); }, play: function() { this.slideshow.playPlayer(); }, pause: function() { this.slideshow.pausePlayer(); }, goToSlide: function(i) { this.slideshow.select(i); }, setDraggable: function(enable) { this.slideshow.options.draggable = enable; this.slideshow.updateDraggable(); }, initChildNav: function() { this.childNavLinks[this.args.initialIndex].classList.add('is-active'); // Setup events this.childNavLinks.forEach((link, i) => { // update data-index because image-set feature may be enabled link.setAttribute('data-index', i); link.addEventListener('click', function(evt) { evt.preventDefault(); this.goToSlide(this.getChildIndex(evt.currentTarget)) }.bind(this)); link.addEventListener('focus', function(evt) { this.goToSlide(this.getChildIndex(evt.currentTarget)) }.bind(this)); link.addEventListener('keydown', function(evt) { if (evt.keyCode === 13) { this.goToSlide(this.getChildIndex(evt.currentTarget)) } }.bind(this)); }); // Setup optional arrows if (this.arrows.length) { this.arrows.forEach(arrow => { arrow.addEventListener('click', this.arrowClick.bind(this)); });; } }, getChildIndex: function(target) { return parseInt(target.dataset.index); }, childNavGoTo: function(index) { this.childNavLinks.forEach(a => { a.blur(); a.classList.remove(classes.isActive); }); var el = this.childNavLinks[index]; el.classList.add(classes.isActive); if (!this.args.childNavScroller) { return; } if (this.args.childVertical) { var elTop = el.offsetTop; this.args.childNavScroller.scrollTop = elTop - 100; } else { var elLeft = el.offsetLeft; this.args.childNavScroller.scrollLeft = elLeft - 100; } }, arrowClick: function(evt) { if (evt.currentTarget.classList.contains('product__thumb-arrow--prev')) { this.slideshow.previous(); } else { this.slideshow.next(); } } }); return slideshow; })(); /*============================================================================ VariantAvailability - Cross out sold out or unavailable variants - To disable, use the Variant Picker Block setting - Required markup: - class=variant-input-wrap to wrap select or button group - class=variant-input to wrap button/label ==============================================================================*/ theme.VariantAvailability = (function() { var classes = { disabled: 'disabled' }; function availability(args) { this.type = args.type; this.variantsObject = args.variantsObject; this.currentVariantObject = args.currentVariantObject; this.container = args.container; this.namespace = args.namespace; this.init(); } availability.prototype = Object.assign({}, availability.prototype, { init: function() { this.container.on('variantChange' + this.namespace, this.setAvailability.bind(this)); // Set default state based on current selected variant this.setInitialAvailability(); }, // Create a list of all options. If any variant exists and is in stock with that option, it's considered available createAvailableOptionsTree(variants, currentlySelectedValues) { // Reduce variant array into option availability tree return variants.reduce((options, variant) => { // Check each option group (e.g. option1, option2, option3) of the variant Object.keys(options).forEach(index => { if (variant[index] === null) return; let entry = options[index].find(option => option.value === variant[index]); if (typeof entry === 'undefined') { // If option has yet to be added to the options tree, add it entry = {value: variant[index], soldOut: true} options[index].push(entry); } const currentOption1 = currentlySelectedValues.find(({value, index}) => index === 'option1') const currentOption2 = currentlySelectedValues.find(({value, index}) => index === 'option2') switch (index) { case 'option1': // Option1 inputs should always remain enabled based on all available variants entry.soldOut = entry.soldOut && variant.available ? false : entry.soldOut; break; case 'option2': // Option2 inputs should remain enabled based on available variants that match first option group if (currentOption1 && variant['option1'] === currentOption1.value) { entry.soldOut = entry.soldOut && variant.available ? false : entry.soldOut; } case 'option3': // Option 3 inputs should remain enabled based on available variants that match first and second option group if ( currentOption1 && variant['option1'] === currentOption1.value && currentOption2 && variant['option2'] === currentOption2.value ) { entry.soldOut = entry.soldOut && variant.available ? false : entry.soldOut; } } }) return options; }, { option1: [], option2: [], option3: []}) }, setInitialAvailability: function() { this.container.querySelectorAll('.variant-input-wrap').forEach(group => { this.disableVariantGroup(group); }); const currentlySelectedValues = this.currentVariantObject.options.map((value,index) => {return {value, index: `option${index+1}`}}) const initialOptions = this.createAvailableOptionsTree(this.variantsObject, currentlySelectedValues, this.currentVariantObject); for (var [option, values] of Object.entries(initialOptions)) { this.manageOptionState(option, values); } }, setAvailability: function(evt) { const {value: lastSelectedValue, index: lastSelectedIndex, currentlySelectedValues, variant} = evt.detail; // Object to hold all options by value. // This will be what sets a button/dropdown as // sold out or unavailable (not a combo set as purchasable) const valuesToManage = this.createAvailableOptionsTree(this.variantsObject, currentlySelectedValues, variant, lastSelectedIndex, lastSelectedValue) // Loop through all option levels and send each // value w/ args to function that determines to show/hide/enable/disable for (var [option, values] of Object.entries(valuesToManage)) { this.manageOptionState(option, values, lastSelectedValue); } }, manageOptionState: function(option, values) { var group = this.container.querySelector('.variant-input-wrap[data-index="'+ option +'"]'); // Loop through each option value values.forEach(obj => { this.enableVariantOption(group, obj); }); }, enableVariantOption: function(group, obj) { // Selecting by value so escape it var value = obj.value.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/@])/g,'\\$1'); if (this.type === 'dropdown') { if (obj.soldOut) { group.querySelector('option[value="'+ value +'"]').disabled = true; } else { group.querySelector('option[value="'+ value +'"]').disabled = false; } } else { var buttonGroup = group.querySelector('.variant-input[data-value="'+ value +'"]'); var input = buttonGroup.querySelector('input'); var label = buttonGroup.querySelector('label'); // Variant exists - enable & show variant input.classList.remove(classes.disabled); label.classList.remove(classes.disabled); // Variant sold out - cross out option (remains selectable) if (obj.soldOut) { input.classList.add(classes.disabled); label.classList.add(classes.disabled); } } }, disableVariantGroup: function(group) { if (this.type === 'dropdown') { group.querySelectorAll('option').forEach(option => { option.disabled = true; }); } else { group.querySelectorAll('input').forEach(input => { input.classList.add(classes.disabled); }); group.querySelectorAll('label').forEach(label => { label.classList.add(classes.disabled); }); } } }); return availability; })(); // Video modal will auto-initialize for any anchor link that points to YouTube // MP4 videos must manually be enabled with: // - .product-video-trigger--mp4 (trigger button) // - .product-video-mp4-sound video player element (cloned into modal) // - see media.liquid for example of this theme.videoModal = function() { var youtubePlayer; var vimeoPlayer; var videoHolderId = 'VideoHolder'; var selectors = { youtube: 'a[href*="youtube.com/watch"], a[href*="youtu.be/"]', vimeo: 'a[href*="player.vimeo.com/player/"], a[href*="vimeo.com/"]', mp4Trigger: '.product-video-trigger--mp4', mp4Player: '.product-video-mp4-sound' }; var youtubeTriggers = document.querySelectorAll(selectors.youtube); var vimeoTriggers = document.querySelectorAll(selectors.vimeo); var mp4Triggers = document.querySelectorAll(selectors.mp4Trigger); if (!youtubeTriggers.length && !vimeoTriggers.length && !mp4Triggers.length) { return; } var videoHolderDiv = document.getElementById(videoHolderId); if (youtubeTriggers.length) { theme.LibraryLoader.load('youtubeSdk'); } if (vimeoTriggers.length) { theme.LibraryLoader.load('vimeo', window.vimeoApiReady); } var modal = new theme.Modals('VideoModal', 'video-modal', { closeOffContentClick: true, bodyOpenClass: ['modal-open', 'video-modal-open'], solid: true }); youtubeTriggers.forEach(btn => { btn.addEventListener('click', triggerYouTubeModal); }); vimeoTriggers.forEach(btn => { btn.addEventListener('click', triggerVimeoModal); }); mp4Triggers.forEach(btn => { btn.addEventListener('click', triggerMp4Modal); }); document.addEventListener('modalClose.VideoModal', closeVideoModal); function triggerYouTubeModal(evt) { // If not already loaded, treat as normal link if (!theme.config.youTubeReady) { return; } evt.preventDefault(); emptyVideoHolder(); modal.open(evt); var videoId = getYoutubeVideoId(evt.currentTarget.getAttribute('href')); youtubePlayer = new theme.YouTube( videoHolderId, { videoId: videoId, style: 'sound', events: { onReady: onYoutubeReady } } ); } function triggerVimeoModal(evt) { // If not already loaded, treat as normal link if (!theme.config.vimeoReady) { return; } evt.preventDefault(); emptyVideoHolder(); modal.open(evt); var videoId = evt.currentTarget.dataset.videoId; var videoLoop = evt.currentTarget.dataset.videoLoop; vimeoPlayer = new theme.VimeoPlayer( videoHolderId, videoId, { style: 'sound', loop: videoLoop, } ); } function triggerMp4Modal(evt) { emptyVideoHolder(); var el = evt.currentTarget; var player = el.parentNode.querySelector(selectors.mp4Player); // Clone video element and place it in the modal var playerClone = player.cloneNode(true); playerClone.classList.remove('hide'); videoHolderDiv.append(playerClone); modal.open(evt); // Play new video element videoHolderDiv.querySelector('video').play(); } function onYoutubeReady(evt) { evt.target.unMute(); evt.target.playVideo(); } function getYoutubeVideoId(url) { var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/; var match = url.match(regExp); return (match&&match[7].length==11)? match[7] : false; } function emptyVideoHolder() { videoHolderDiv.innerHTML = ''; } function closeVideoModal() { if (youtubePlayer && typeof youtubePlayer.destroy === 'function') { youtubePlayer.destroy(); } else if (vimeoPlayer && typeof vimeoPlayer.destroy === 'function') { vimeoPlayer.destroy(); } else { emptyVideoHolder(); } } }; /*============================================================================ ToolTip ==============================================================================*/ class ToolTip extends HTMLElement { constructor() { super(); this.el = this; this.inner = this.querySelector('[data-tool-tip-inner]'); this.closeButton = this.querySelector('[data-tool-tip-close]'); this.toolTipContent = this.querySelector('[data-tool-tip-content]'); this.toolTipTitle = this.querySelector('[data-tool-tip-title]'); this.triggers = document.querySelectorAll('[data-tool-tip-trigger]'); document.addEventListener('tooltip:open', e => { this._open(e.detail.context, e.detail.content); }); } _open(context, insertedHtml) { this.toolTipContent.innerHTML = insertedHtml; // Ensure we set a title for product availability if (context != 'store-availability') { this.toolTipTitle.remove(); } this._lockScrolling(); if (this.closeButton) { this.closeButton.on('click' + '.tooltip-close', () => { this._close(); }); } document.documentElement.on('click' + '.tooltip-outerclick', event => { if (this.el.dataset.toolTipOpen === 'true' && !this.inner.contains(event.target)) this._close(); }); document.documentElement.on('keydown' + '.tooltip-esc', event => { if (event.code === 'Escape') this._close(); }); this.el.dataset.toolTipOpen = true; this.el.dataset.toolTip = context; } _close() { this.toolTipContent.innerHTML = ''; this.el.dataset.toolTipOpen = 'false'; this.el.dataset.toolTip = ''; this._unlockScrolling(); this.closeButton.off('click' + '.tooltip-close'); document.documentElement.off('click' + '.tooltip-outerclick'); document.documentElement.off('keydown' + '.tooltip-esc'); } _lockScrolling() { theme.a11y.trapFocus({ container: this.el, namespace: 'tooltip_focus' }); theme.a11y.lockMobileScrolling(); document.documentElement.classList.add('modal-open'); } _unlockScrolling() { theme.a11y.removeTrapFocus({ container: this.el, namespace: 'tooltip_focus' }); theme.a11y.unlockMobileScrolling(); document.documentElement.classList.remove('modal-open'); } } customElements.define('tool-tip', ToolTip); /*============================================================================ ToolTipTrigger ==============================================================================*/ class ToolTipTrigger extends HTMLElement { constructor() { super(); this.el = this; this.toolTipContent = this.querySelector('[data-tool-tip-content]'); this.init(); } init() { const toolTipOpen = new CustomEvent('tooltip:open', { detail: { context: this.dataset.toolTip, content: this.toolTipContent.innerHTML }, bubbles: true }); this.el.addEventListener('click', e => { e.stopPropagation(); this.dispatchEvent(toolTipOpen); }); } } customElements.define('tool-tip-trigger', ToolTipTrigger); /*============================================================================ NewsletterReminder ==============================================================================*/ class NewsletterReminder extends HTMLElement { constructor() { super(); this.closeBtn = this.querySelector('[data-close-button]'); this.popupTrigger = this.querySelector('[data-message]'); this.id = this.dataset.sectionId; this.newsletterId = `NewsletterPopup-${ this.id }`; this.cookie = Cookies.get(`newsletter-${this.id}`); this.cookieName = `newsletter-${this.id}`; this.secondsBeforeShow = this.dataset.delaySeconds; this.expiry = parseInt(this.dataset.delayDays); this.modal = new theme.Modals(`NewsletterPopup-${this.newsletterId}`, 'newsletter-popup-modal'); this.init(); } init() { document.addEventListener('shopify:block:select', (evt) => { if (evt.detail.sectionId === this.id) { this.show(0, true) } }); document.addEventListener('shopify:block:deselect', (evt) => { if (evt.detail.sectionId === this.id) { this.hide(); } }); document.addEventListener(`modalOpen.${this.newsletterId}`, () => this.hide()); document.addEventListener(`modalClose.${this.newsletterId}`, () => this.show()); document.addEventListener(`newsletter:openReminder`, () => this.show(0)); this.closeBtn.addEventListener('click', () => { this.hide(); Cookies.set(this.cookieName, 'opened', { path: '/', expires: this.expiry }); }); this.popupTrigger.addEventListener('click', () => { const reminderOpen = new CustomEvent('reminder:openNewsletter', { bubbles: true }); this.dispatchEvent(reminderOpen); this.hide(); }); } show(time = this.secondsBeforeShow, forceOpen = false) { const reminderAppeared = (sessionStorage.getItem('reminderAppeared') === 'true'); if (!reminderAppeared) { setTimeout(() => { this.dataset.enabled = 'true'; sessionStorage.setItem('reminderAppeared', true); }, time * 1000); } } hide() { this.dataset.enabled = 'false'; } } customElements.define('newsletter-reminder', NewsletterReminder); theme.announcementBar = (function() { var args = { autoPlay: 5000, avoidReflow: true, cellAlign: theme.config.rtl ? 'right' : 'left' }; var bar; var flickity; function init() { bar = document.getElementById('AnnouncementSlider'); if (!bar) { return; } unload(); if (bar.dataset.blockCount === 1) { return; } if (theme.config.bpSmall || bar.dataset.compact === 'true') { initSlider(); } document.addEventListener('matchSmall', function() { unload(); initSlider(); }); document.addEventListener('unmatchSmall', function() { unload(); if (bar.dataset.compact === 'true') { initSlider(); } }); } function initSlider() { flickity = new theme.Slideshow(bar, args); } // Go to slide if selected in the editor function onBlockSelect(id) { var slide = bar.querySelector('#AnnouncementSlide-' + id); var index = parseInt(slide.dataset.index); if (flickity && typeof flickity.pause === 'function') { flickity.goToSlide(index); flickity.pause(); } } function onBlockDeselect() { if (flickity && typeof flickity.play === 'function') { flickity.play(); } } function unload() { if (flickity && typeof flickity.destroy === 'function') { flickity.destroy(); } } return { init: init, onBlockSelect: onBlockSelect, onBlockDeselect: onBlockDeselect, unload: unload }; })(); theme.customerTemplates = function() { checkUrlHash(); initEventListeners(); resetPasswordSuccess(); customerAddressForm(); function checkUrlHash() { var hash = window.location.hash; // Allow deep linking to recover password form if (hash === '#recover') { toggleRecoverPasswordForm(); } } function toggleRecoverPasswordForm() { var passwordForm = document.getElementById('RecoverPasswordForm').classList.toggle('hide'); var loginForm = document.getElementById('CustomerLoginForm').classList.toggle('hide'); } function initEventListeners() { // Show reset password form var recoverForm = document.getElementById('RecoverPassword'); if (recoverForm) { recoverForm.addEventListener('click', function(evt) { evt.preventDefault(); toggleRecoverPasswordForm(); }); } // Hide reset password form var hideRecoverPassword = document.getElementById('HideRecoverPasswordLink'); if (hideRecoverPassword) { hideRecoverPassword.addEventListener('click', function(evt) { evt.preventDefault(); toggleRecoverPasswordForm(); }); } } function resetPasswordSuccess() { var formState = document.querySelector('.reset-password-success'); // check if reset password form was successfully submitted if (!formState) { return; } // show success message document.getElementById('ResetSuccess').classList.remove('hide'); } function customerAddressForm() { var newAddressForm = document.getElementById('AddressNewForm'); var addressForms = document.querySelectorAll('.js-address-form'); if (!newAddressForm || !addressForms.length) { return; } // Country/province selector can take a short time to load setTimeout(function() { document.querySelectorAll('.js-address-country').forEach(el => { var countryId = el.dataset.countryId; var provinceId = el.dataset.provinceId; var provinceContainerId = el.dataset.provinceContainerId; new Shopify.CountryProvinceSelector( countryId, provinceId, { hideElement: provinceContainerId } ); }); }, 1000); // Toggle new/edit address forms document.querySelectorAll('.address-new-toggle').forEach(el => { el.addEventListener('click', function() { newAddressForm.classList.toggle('hide'); }); }); document.querySelectorAll('.address-edit-toggle').forEach(el => { el.addEventListener('click', function(evt) { var formId = evt.currentTarget.dataset.formId; document.getElementById('EditAddress_' + formId).classList.toggle('hide'); }); }); document.querySelectorAll('.address-delete').forEach(el => { el.addEventListener('click', function(evt) { var formId = evt.currentTarget.dataset.formId; var confirmMessage = evt.currentTarget.dataset.confirmMessage; if (confirm(confirmMessage || 'Are you sure you wish to delete this address?')) { if (Shopify) { Shopify.postLink('/account/addresses/' + formId, {parameters: {_method: 'delete'}}); } } }) }); } }; theme.CartDrawer = (function() { var selectors = { drawer: '#CartDrawer', form: '#CartDrawerForm' }; function CartDrawer() { this.form = document.querySelector(selectors.form); this.drawer = new theme.Drawers('CartDrawer', 'cart'); this.init(); } CartDrawer.prototype = Object.assign({}, CartDrawer.prototype, { init: function() { this.cartForm = new theme.CartForm(this.form); this.cartForm.buildCart(); document.addEventListener('ajaxProduct:added', function(evt) { this.cartForm.buildCart(); this.open(); }.bind(this)); // Dev-friendly way to open cart document.addEventListener('cart:open', this.open.bind(this)); document.addEventListener('cart:close', this.close.bind(this)); }, open: function() { this.drawer.open(); }, close: function() { this.drawer.close(); } }); return CartDrawer; })(); theme.headerNav = (function() { var selectors = { wrapper: '#HeaderWrapper', siteHeader: '#SiteHeader', searchBtn: '.js-search-header', closeSearch: '#SearchClose', searchContainer: '.site-header__search-container', logo: '#LogoContainer img', megamenu: '.megamenu', navItems: '.site-nav__item', navLinks: '.site-nav__link', navLinksWithDropdown: '.site-nav__link--has-dropdown', navDropdownLinks: '.site-nav__dropdown-link--second-level' }; var classes = { hasDropdownClass: 'site-nav--has-dropdown', hasSubDropdownClass: 'site-nav__deep-dropdown-trigger', dropdownActive: 'is-focused' }; var config = { namespace: '.siteNav', wrapperOverlayed: false, overlayedClass: 'is-light', overlayEnabledClass: 'header-wrapper--sticky', stickyEnabled: false, stickyActive: false, stickyClass: 'site-header--stuck', stickyHeaderWrapper: 'StickyHeaderWrap', openTransitionClass: 'site-header--opening', lastScroll: 0 }; // Elements used in resize functions, defined in init var wrapper; var siteHeader; function init() { wrapper = document.querySelector(selectors.wrapper); siteHeader = document.querySelector(selectors.siteHeader); config.stickyEnabled = (siteHeader.dataset.sticky === 'true'); if (config.stickyEnabled) { config.wrapperOverlayed = wrapper.classList.contains(config.overlayedClass); stickyHeaderCheck(); } theme.settings.overlayHeader = (siteHeader.dataset.overlay === 'true'); // Disable overlay header if on collection template with no collection image if (theme.settings.overlayHeader && Shopify && Shopify.designMode) { if (document.body.classList.contains('template-collection') && !document.querySelector('.collection-hero')) { this.disableOverlayHeader(); } } accessibleDropdowns(); searchDrawer(); } // If the header setting to overlay the menu on the collection image // is enabled but the collection setting is disabled, we need to undo // the init of the sticky nav function disableOverlayHeader() { wrapper.classList.remove(config.overlayEnabledClass, config.overlayedClass); config.wrapperOverlayed = false; theme.settings.overlayHeader = false; } function stickyHeaderCheck() { // Disable sticky header if any mega menu is taller than window theme.config.stickyHeader = doesMegaMenuFit(); if (theme.config.stickyHeader) { config.forceStopSticky = false; stickyHeader(); } else { config.forceStopSticky = true; } } function doesMegaMenuFit() { var largestMegaNav = 0; siteHeader.querySelectorAll(selectors.megamenu).forEach(nav => { var h = nav.offsetHeight; if (h > largestMegaNav) { largestMegaNav = h; } }); // 120 ~ space of visible header when megamenu open if (window.innerHeight < (largestMegaNav + 120)) { return false; } return true; } function stickyHeader() { config.lastScroll = 0; var wrapWith = document.createElement('div'); wrapWith.id = config.stickyHeaderWrapper; theme.utils.wrap(siteHeader, wrapWith); stickyHeaderInitialPosition(siteHeader); stickyHeaderHeight(); window.on('resize' + config.namespace, theme.utils.debounce(50, stickyHeaderHeight)); window.on('scroll' + config.namespace, theme.utils.throttle(20, stickyHeaderScroll)); // This gets messed up in the editor, so here's a fix if (Shopify && Shopify.designMode) { setTimeout(function() { stickyHeaderHeight(); }, 250); } } function stickyHeaderInitialPosition(header) { const headerParent = header.closest('.shopify-section-group-header-group'); const parentNextSibling = headerParent.nextElementSibling; // if parentNextSibling has same class as headerParent, then header is above announcement bar if (parentNextSibling && parentNextSibling.classList.contains('shopify-section-group-header-group')) { // get height of announcement bar and set header wrapper top to that value const nextSiblingHeight = parentNextSibling.offsetHeight; document.querySelector(selectors.wrapper).style.top = nextSiblingHeight + 'px'; } } function stickyHeaderHeight() { if (!config.stickyEnabled) { return; } var h = siteHeader.offsetHeight; // If sticky header is 'active' i.e. we have scrolled a bit down the page // the site header has 20px less padding when sticky // and this is not factored into the height calculation // The 20px is what causes the header to overlap the announcement bar // This only applies when the height is being calculated on larger screen sizes if (siteHeader.classList.contains('site-header--stuck') && !theme.config.bpSmall) { let siteHeaderPadding = parseFloat(window.getComputedStyle(siteHeader, null).getPropertyValue('padding-top')); h += siteHeaderPadding * 2; } var stickyHeader = document.querySelector('#' + config.stickyHeaderWrapper); stickyHeader.style.height = h + 'px'; } function stickyHeaderScroll() { if (!config.stickyEnabled) { return; } if (config.forceStopSticky) { return; } requestAnimationFrame(scrollHandler); config.lastScroll = window.scrollY; } function scrollHandler() { if (window.scrollY > 250) { if (config.stickyActive) { return; } config.stickyActive = true; siteHeader.classList.add(config.stickyClass); if (config.wrapperOverlayed) { wrapper.classList.remove(config.overlayedClass); } // Add open transition class after element is set to fixed // so CSS animation is applied correctly setTimeout(function() { siteHeader.classList.add(config.openTransitionClass); }, 100); } else { if (!config.stickyActive) { return; } config.stickyActive = false; siteHeader.classList.remove(config.openTransitionClass); siteHeader.classList.remove(config.stickyClass); if (config.wrapperOverlayed) { wrapper.classList.add(config.overlayedClass); } } } function accessibleDropdowns() { var hasActiveDropdown = false; var hasActiveSubDropdown = false; var closeOnClickActive = false; // Touch devices open dropdown on first click, navigate to link on second if (theme.config.isTouch) { document.querySelectorAll(selectors.navLinksWithDropdown).forEach(el => { el.on('touchend' + config.namespace, function(evt) { var parent = evt.currentTarget.parentNode; if (!parent.classList.contains(classes.dropdownActive)) { evt.preventDefault(); closeDropdowns(); openFirstLevelDropdown(evt.currentTarget); } else { window.location.replace(evt.currentTarget.getAttribute('href')); } }); }); } // Open/hide top level dropdowns document.querySelectorAll(selectors.navLinks).forEach(el => { el.on('focusin' + config.namespace, accessibleMouseEvent); el.on('mouseover' + config.namespace, accessibleMouseEvent); el.on('mouseleave' + config.namespace, closeDropdowns); }); document.querySelectorAll(selectors.navDropdownLinks).forEach(el => { if (theme.config.isTouch) { el.on('touchend' + config.namespace, function(evt) { var parent = evt.currentTarget.parentNode; // Open third level menu or go to link based on active state if (parent.classList.contains(classes.hasSubDropdownClass)) { if (!parent.classList.contains(classes.dropdownActive)) { evt.preventDefault(); closeThirdLevelDropdown(); openSecondLevelDropdown(evt.currentTarget); } else { window.location.replace(evt.currentTarget.getAttribute('href')); } } else { // No third level nav, go to link window.location.replace(evt.currentTarget.getAttribute('href')); } }); } // Open/hide sub level dropdowns el.on('focusin' + config.namespace, function(evt) { closeThirdLevelDropdown(); openSecondLevelDropdown(evt.currentTarget, true); }) }); // Clicking outside of the megamenu should close it if (theme.config.isTouch) { document.body.on('touchend' + config.namespace, function() { closeDropdowns(); }); // Exception to above: clicking anywhere on the megamenu content will NOT close it siteHeader.querySelectorAll(selectors.megamenu).forEach(el => { el.on('touchend' + config.namespace, function(evt) { evt.stopImmediatePropagation(); }); }); } function accessibleMouseEvent(evt) { if (hasActiveDropdown) { closeSecondLevelDropdown(); } if (hasActiveSubDropdown) { closeThirdLevelDropdown(); } openFirstLevelDropdown(evt.currentTarget); } // Private dropdown functions function openFirstLevelDropdown(el) { var parent = el.parentNode; if (parent.classList.contains(classes.hasDropdownClass)) { parent.classList.add(classes.dropdownActive); hasActiveDropdown = true; } if (!theme.config.isTouch) { if (!closeOnClickActive) { var eventType = theme.config.isTouch ? 'touchend' : 'click'; closeOnClickActive = true; document.documentElement.on(eventType + config.namespace, function() { closeDropdowns(); document.documentElement.off(eventType + config.namespace); closeOnClickActive = false; }.bind(this)); } } } function openSecondLevelDropdown(el, skipCheck) { var parent = el.parentNode; if (parent.classList.contains(classes.hasSubDropdownClass) || skipCheck) { parent.classList.add(classes.dropdownActive); hasActiveSubDropdown = true; } } function closeDropdowns() { closeSecondLevelDropdown(); closeThirdLevelDropdown(); } function closeSecondLevelDropdown() { document.querySelectorAll(selectors.navItems).forEach(el => { el.classList.remove(classes.dropdownActive) }); } function closeThirdLevelDropdown() { document.querySelectorAll(selectors.navDropdownLinks).forEach(el => { el.parentNode.classList.remove(classes.dropdownActive); }); } } function searchDrawer() { document.querySelectorAll(selectors.searchBtn).forEach(btn => { btn.addEventListener('click', openSearchDrawer); }); document.querySelector(selectors.closeSearch).addEventListener('click', closeSearchDrawer); } function openSearchDrawer(evt) { evt.preventDefault(); evt.stopImmediatePropagation(); var container = document.querySelector(selectors.searchContainer); theme.utils.prepareTransition(container, function() { container.classList.add('is-active'); }.bind(this)); document.documentElement.classList.add('js-drawer-open', 'js-drawer-open--search'); setTimeout(function() { theme.a11y.trapFocus({ container: container, namespace: 'header_search', elementToFocus: container.querySelector('.site-header__search-input') }); }, 100); // If sticky is enabled, scroll to top on mobile when close to it // so you don't get an invisible search box if (theme.config.bpSmall && config.stickyEnabled && config.lastScroll < 300) { window.scrollTo(0,0); } // Bind events theme.a11y.lockMobileScrolling(config.namespace); bindSearchEvents(); } function closeSearchDrawer(evt) { // Do not close if click event came from inside drawer if (evt) { // evt.path is non-standard, so have fallback var path = evt.path || (evt.composedPath && evt.composedPath()); for (var i = 0; i < path.length; i++) { if (path[i].classList) { if (path[i].classList.contains('site-header__search-btn')) { break; } if (path[i].classList.contains('site-header__search-container')) { return; } } } } // deselect any focused form elements document.activeElement.blur(); document.documentElement.classList.add('js-drawer-closing'); document.documentElement.classList.remove('js-drawer-open', 'js-drawer-open--search'); window.setTimeout(function() { document.documentElement.classList.remove('js-drawer-closing'); }.bind(this), 500); var container = document.querySelector(selectors.searchContainer); theme.utils.prepareTransition(container, function() { container.classList.remove('is-active'); }.bind(this)); theme.a11y.removeTrapFocus({ container: container, namespace: 'header_search' }); theme.a11y.unlockMobileScrolling(config.namespace); unbindSearchEvents(); } function bindSearchEvents() { window.on('keyup' + config.namespace, function(evt) { if (evt.keyCode === 27) { closeSearchDrawer(); } }.bind(this)); // Clicking out of container closes it document.documentElement.on('click' + config.namespace, function(evt) { closeSearchDrawer(evt); }.bind(this)); } function unbindSearchEvents() { window.off('keyup' + config.namespace); document.documentElement.off('click' + config.namespace); } return { init: init, disableOverlayHeader: disableOverlayHeader }; })(); window.onpageshow = function(evt) { // Removes unload class when returning to page via history if (evt.persisted) { document.body.classList.remove('unloading'); document.querySelectorAll('.cart__checkout').forEach(el => { el.classList.remove('btn--loading'); }); } }; theme.predictiveSearch = (function() { var currentString = ''; var isLoading = false; var searchTimeout; var namespace = '.predictive'; var selectors = { form: '#HeaderSearchForm', input: 'input[type="search"]', wrapper: '#PredictiveWrapper', resultDiv: '#PredictiveResults', searchButton: '[data-predictive-search-button]' }; var cache = {}; var config = { imageSize: 'square' } var classes = { isActive: 'predicitive-active' }; var keys = { up_arrow: 38, down_arrow: 40, tab: 9 }; function init() { // Only some languages support predictive search if (document.getElementById('shopify-features')) { var supportedShopifyFeatures = JSON.parse(document.getElementById('shopify-features').innerHTML); if (!supportedShopifyFeatures.predictiveSearch) { return; } } cache.wrapper = document.querySelector(selectors.wrapper); if (!cache.wrapper) { return; } config.imageSize = cache.wrapper.dataset.imageSize; cache.form = document.querySelector(selectors.form); cache.form.setAttribute('autocomplete', 'off'); cache.form.on('submit' + namespace, submitSearch); cache.input = cache.form.querySelector(selectors.input); cache.input.on('keyup' + namespace, handleKeyup); cache.submit = cache.wrapper.querySelector(selectors.searchButton); cache.submit.on('click' + namespace, triggerSearch); cache.results = document.querySelector(selectors.resultDiv); } function reset() { cache.wrapper.classList.add('hide'); cache.results.innerHTML = ''; clearTimeout(searchTimeout); } function triggerSearch() { cache.form.submit(); } // Append * wildcard to search function submitSearch(evt) { evt.preventDefault ? evt.preventDefault() : evt.returnValue = false; var obj = {}; var formData = new FormData(evt.target); for (var key of formData.keys()) { obj[key] = formData.get(key); } if (obj.q) { obj.q += '*'; } var params = paramUrl(obj); window.location.href = `${theme.routes.search}?${params}`; return false; } function handleKeyup(evt) { if (evt.keyCode === keys.up_arrow) { return; } if (evt.keyCode === keys.down_arrow) { return; } if (evt.keyCode === keys.tab) { return; } search(); } function search() { var keyword = cache.input.value; if (keyword === '') { reset(); return; } var q = _normalizeQuery(keyword); clearTimeout(searchTimeout); searchTimeout = setTimeout( function () { predictQuery(q); }.bind(this), 500 ); } function predictQuery(q) { if (isLoading) { return; } // Do not re-search the same thing if (currentString === q) { return; } currentString = q; isLoading = true; var searchObj = { 'q': q, 'resources[type]': theme.settings.predictiveSearchType, 'resources[limit]': 4, 'resources[options][unavailable_products]': 'last', 'resources[options][fields]': 'title,product_type,variants.title,vendor' }; var params = paramUrl(searchObj); var requestResponse; var predictiveSearchSection = document.getElementById('PredictiveResults'); var predictiveWrapper = document.getElementById('PredictiveWrapper'); fetch(`${theme.routes.predictive_url}?${params}§ion_id=predictive-search`) .then((response) => { isLoading = false; requestResponse = response; return response.text(); }) .then((text) => { if (!requestResponse.ok) { throw new Error(`${requestResponse.status}: ${text}`); } const resultsMarkup = new DOMParser() .parseFromString(text, 'text/html') .querySelector('#shopify-section-predictive-search').innerHTML; const checkMarkup = resultsMarkup.replace(/\s+/g, ''); if (checkMarkup == '') { reset(); return; } predictiveSearchSection.innerHTML = resultsMarkup; predictiveWrapper.classList.remove('hide'); }) .catch((error) => { console.error(error); }); } function _normalizeQuery(string) { if (typeof string !== 'string') { return null; } return string .trim() .replace(/\ /g, '-') .toLowerCase(); } function paramUrl(obj) { return Object.keys(obj).map(function(key) { return key + '=' + encodeURIComponent(obj[key]); }).join('&') } return { init: init }; })(); theme.buildProductGridItem = function(items, gridWidth, rowOf, imageSizes) { var output = ''; items.forEach(product => { var image = theme.buildProductImage(product, imageSizes); let priceMarkup = ''; let vendorMarkup = ''; if (theme.settings.predictiveSearchPrice) priceMarkup = `<div class="grid-product__price">${theme.strings.productFrom}${theme.Currency.formatMoney(product.price_min, theme.moneyFormat)}</div>`; if (theme.settings.predictiveSearchVendor) vendorMarkup = `<div class="grid-product__vendor">${product.vendor}</div>`; var markup = ` <div class="grid__item grid-product ${gridWidth} aos-animate" data-aos="row-of-${rowOf}"> <div class="grid-product__content"> <a href="${product.url}" class="grid-product__link"> <div class="grid-product__image-mask"> ${image} </div> <div class="grid-product__meta"> <div class="grid-product__title">${product.title}</div> ${priceMarkup} ${vendorMarkup} </div> </a> </div> </div> `; output += markup; }); return output; } theme.buildProductImage = function(product, imageSizes) { var size = theme.settings.productImageSize; var output = ''; if (size === 'natural') { const template = document.getElementById("naturalImageMarkup"); const clonedMarkup = template.content.cloneNode(true); const imageEl = clonedMarkup.querySelector('img'); const imageWrapperEl = clonedMarkup.querySelector('.image-wrap'); imageWrapperEl.style.paddingBottom = `${product.image_aspect_ratio}%`; addImageProperties(imageEl, imageSizes); const imageDiv = document.createElement('div'); imageDiv.appendChild(clonedMarkup); output = imageDiv.innerHTML; } else { const template = document.getElementById("fixedRatioImageMarkup"); const clonedMarkup = template.content.cloneNode(true); const imageEl = clonedMarkup.querySelector('img'); const imageWrapperEl = clonedMarkup.querySelector('.grid__image-ratio'); imageWrapperEl.classList.add(`grid__image-ratio--${size}`); if (!theme.settings.productImageCover) { imageEl.classList.add('grid__image-contain'); } addImageProperties(imageEl, imageSizes); const imageDiv = document.createElement('div'); imageDiv.appendChild(clonedMarkup); output = imageDiv.innerHTML; } function addImageProperties(imageEl, imageSizes) { imageEl.src = product.image_responsive_url; imageEl.srcset = product.image_responsive_urls.toString(); imageEl.alt = product.title; imageEl.sizes = imageSizes; } return output; } theme.buildCollectionItem = function(items) { var output = ''; items.forEach(collection => { var markup = ` <li> <a href="${collection.url}"> ${collection.title} </a> </li> `; output += markup; }); return output; } theme.buildPageItem = function(items) { var output = ''; items.forEach(page => { var markup = ` <li> <a href="${page.url}"> ${page.title} </a> </li> `; output += markup; }); return output; } theme.buildArticleItem = function(items, imageSize) { var output = ''; items.forEach(article => { var image = theme.buildPredictiveImage(article); var markup = ` <div class="grid__item grid-product small--one-half medium-up--one-quarter" data-aos="row-of-4"> <a href="${article.url}" class="grid-product__link grid-product__link--inline"> <div class="grid-product__image-mask"> <div class="grid__image-ratio grid__image-ratio--object grid__image-ratio--${imageSize}"> <div class="predictive__image-wrap"> ${image} </div> </div> </div> <div class="grid-product__meta"> ${article.title} </div> </a> </div> `; output += markup; }); return output; } theme.buildPredictiveImage = function(obj) { var imageMarkup = ''; if (obj.image) { const template = document.getElementById("articleImageMarkup"); const clonedMarkup = template.content.cloneNode(true); const imageEl = clonedMarkup.querySelector('img'); imageEl.src = obj.image; const imageDiv = document.createElement('div'); imageDiv.appendChild(clonedMarkup); imageMarkup = imageDiv.innerHTML; } return imageMarkup; } /*============================================================================ Age Verification Popup ==============================================================================*/ class AgeVerificationPopup extends HTMLElement { constructor() { super(); this.cookieName = this.id; this.cookie = Cookies.get(this.cookieName); this.classes = { activeContent: 'age-verification-popup__content--active', inactiveContent: 'age-verification-popup__content--inactive', inactiveDeclineContent: 'age-verification-popup__decline-content--inactive', activeDeclineContent: 'age-verification-popup__decline-content--active', } this.declineButton = this.querySelector('[data-age-verification-popup-decline-button]'); this.declineContent = this.querySelector('[data-age-verification-popup-decline-content]'); this.content = this.querySelector('[data-age-verification-popup-content]'); this.returnButton = this.querySelector('[data-age-verification-popup-return-button]'); this.exitButton = this.querySelector('[data-age-verification-popup-exit-button]'); this.backgroundImage = this.querySelector('[data-background-image]'); this.mobileBackgroundImage = this.querySelector('[data-mobile-background-image]'); if (Shopify.designMode) { document.addEventListener('shopify:section:select', (event) => { if (event.detail.sectionId === this.dataset.sectionId) { this.init(); } }) document.addEventListener('shopify:section:load', (event) => { if (event.detail.sectionId === this.dataset.sectionId) { this.init(); // If 'Test mode' is enabled, remove the cookie we've set if (this.dataset.testMode === 'true' && this.cookie) { Cookies.remove(this.cookieName); } // Check session storage if user was editing on the second view const secondViewVisited = sessionStorage.getItem(this.id); if (!secondViewVisited) return; this.showDeclineContent(); } }) document.addEventListener('shopify:section:unload', (event) => { if (event.detail.sectionId === this.dataset.sectionId) { this.modal.close(); } }) } // Age verification popup will only be hidden if test mode is disabled AND // either a cookie exists OR visibility is toggled in the editor if ((this.cookie) && this.dataset.testMode === 'false') return; this.init(); } init() { this.modal = new theme.Modals(this.id, 'age-verification-popup-modal', { closeOffContentClick: false }); if (this.backgroundImage) { this.backgroundImage.style.display = 'block'; } if (theme.config.bpSmall && this.mobileBackgroundImage) { this.mobileBackgroundImage.style.display = 'block'; } this.modal.open(); theme.a11y.lockMobileScrolling(`#${this.id}`, document.querySelector('#MainContent')); if (this.declineButton) { this.declineButton.addEventListener('click', (e) => { e.preventDefault(); this.showDeclineContent(); // If in editor, save to session storage to indicate that user has moved on to the second view // Allows view to persist while making changes in the editor if (Shopify.designMode) { sessionStorage.setItem(this.id, 'second-view'); } }); } if (this.returnButton) { this.returnButton.addEventListener('click', (e) => { e.preventDefault(); this.hideDeclineContent(); // Remove data from session storage so second view doesn't persist const secondViewVisited = sessionStorage.getItem(this.id); if (Shopify.designMode && secondViewVisited) { sessionStorage.removeItem(this.id); } }) } if (this.exitButton) { this.exitButton.addEventListener('click', (e) => { e.preventDefault(); // We don't want to set a cookie if in test mode if (this.dataset.testMode === 'false') { Cookies.set(this.cookieName, 'entered', { expires: 30 }); } if (this.backgroundImage) { this.backgroundImage.style.display = 'none'; } if (theme.config.bpSmall && this.mobileBackgroundImage) { this.mobileBackgroundImage.style.display = 'none'; } this.modal.close(); theme.a11y.unlockMobileScrolling(`#${this.id}`, document.querySelector('#MainContent')); }) } } showDeclineContent() { this.declineContent.classList.remove(this.classes.inactiveDeclineContent); this.declineContent.classList.add(this.classes.activeDeclineContent); this.content.classList.add(this.classes.inactiveContent); this.content.classList.remove(this.classes.activeContent); } hideDeclineContent() { this.declineContent.classList.add(this.classes.inactiveDeclineContent); this.declineContent.classList.remove(this.classes.activeDeclineContent); this.content.classList.remove(this.classes.inactiveContent); this.content.classList.add(this.classes.activeContent); } } customElements.define('age-verification-popup', AgeVerificationPopup); theme.Maps = (function() { var config = { zoom: 14 }; var apiStatus = null; var mapsToLoad = []; var errors = {}; var selectors = { section: '[data-section-type="map"]', map: '[data-map]', mapOverlay: '.map-section__overlay' }; // Global function called by Google on auth errors. // Show an auto error message on all map instances. window.gm_authFailure = function() { if (!Shopify.designMode) { return; } document.querySelectorAll(selectors.section).forEach(section => { section.classList.add('map-section--load-error'); }); document.querySelectorAll(selectors.map).forEach(map => { map.parentNode.removeChild(map); }); window.mapError(theme.strings.authError); }; window.mapError = function(error) { var message = document.createElement('div'); message.classList.add('map-section__error', 'errors', 'text-center'); message.innerHTML = error; document.querySelectorAll(selectors.mapOverlay).forEach(overlay => { overlay.parentNode.prepend(message); }); document.querySelectorAll('.map-section__link').forEach(link => { link.classList.add('hide'); }); }; function Map(container) { this.container = container; this.sectionId = this.container.getAttribute('data-section-id'); this.namespace = '.map-' + this.sectionId; this.map = container.querySelector(selectors.map); this.key = this.map.dataset.apiKey; errors = { addressNoResults: theme.strings.addressNoResults, addressQueryLimit: theme.strings.addressQueryLimit, addressError: theme.strings.addressError, authError: theme.strings.authError }; if (!this.key) { return; } theme.initWhenVisible({ element: this.container, callback: this.prepMapApi.bind(this), threshold: 20 }); } // API has loaded, load all Map instances in queue function initAllMaps() { mapsToLoad.forEach(instance => { instance.createMap(); }); } function geolocate(map) { var geocoder = new google.maps.Geocoder(); if (!map) { return; } var address = map.dataset.addressSetting; var deferred = new Promise((resolve, reject) => { geocoder.geocode({ address: address }, function(results, status) { if (status !== google.maps.GeocoderStatus.OK) { reject(status); } resolve(results); }); }); return deferred; } Map.prototype = Object.assign({}, Map.prototype, { prepMapApi: function() { if (apiStatus === 'loaded') { this.createMap(); } else { mapsToLoad.push(this); if (apiStatus !== 'loading') { apiStatus = 'loading'; if (typeof window.google === 'undefined' || typeof window.google.maps === 'undefined' ) { var script = document.createElement('script'); script.onload = function () { apiStatus = 'loaded'; initAllMaps(); }; script.src = 'https://maps.googleapis.com/maps/api/js?key=' + this.key; document.head.appendChild(script); } } } }, createMap: function() { var mapDiv = this.map; return geolocate(mapDiv) .then( function(results) { var mapOptions = { zoom: config.zoom, backgroundColor: 'none', center: results[0].geometry.location, draggable: false, clickableIcons: false, scrollwheel: false, disableDoubleClickZoom: true, disableDefaultUI: true }; var map = (this.map = new google.maps.Map(mapDiv, mapOptions)); var center = (this.center = map.getCenter()); var marker = new google.maps.Marker({ map: map, position: map.getCenter() }); google.maps.event.addDomListener( window, 'resize', theme.utils.debounce(250, function() { google.maps.event.trigger(map, 'resize'); map.setCenter(center); mapDiv.removeAttribute('style'); }) ); if (Shopify.designMode) { if (window.AOS) { AOS.refreshHard() } } }.bind(this) ) .catch(function(status) { var errorMessage; switch (status) { case 'ZERO_RESULTS': errorMessage = errors.addressNoResults; break; case 'OVER_QUERY_LIMIT': errorMessage = errors.addressQueryLimit; break; case 'REQUEST_DENIED': errorMessage = errors.authError; break; default: errorMessage = errors.addressError; break; } // Show errors only to merchant in the editor. if (Shopify.designMode) { window.mapError(errorMessage); } }); }, onUnload: function() { if (this.map.length === 0) { return; } // Causes a harmless JS error when a section without an active map is reloaded if (google && google.maps && google.maps.event) { google.maps.event.clearListeners(this.map, 'resize'); } } }); return Map; })(); theme.NewsletterPopup = (function () { function NewsletterPopup(container) { this.container = container; var sectionId = this.container.getAttribute('data-section-id'); this.cookieName = 'newsletter-' + sectionId; this.cookie = Cookies.get(this.cookieName); if (!container) { return; } // Prevent popup on Shopify robot challenge page if (window.location.pathname === '/challenge') { return; } // Prevent popup on password page if (window.location.pathname === '/password') { return; } this.data = { secondsBeforeShow: container.dataset.delaySeconds, daysBeforeReappear: container.dataset.delayDays, hasReminder: container.dataset.hasReminder, testMode: container.dataset.testMode }; this.modal = new theme.Modals('NewsletterPopup-' + sectionId, 'newsletter-popup-modal'); // Set cookie if optional button is clicked var btn = container.querySelector('.popup-cta a'); if (btn) { btn.addEventListener('click', function () { this.closePopup(true); }.bind(this)); } // Open modal if errors or success message exist if (container.querySelector('.errors') || container.querySelector('.note--success')) { this.modal.open(); } // Set cookie as opened if success message if (container.querySelector('.note--success')) { this.closePopup(true); return; } document.addEventListener('modalClose.' + container.id, this.closePopup.bind(this)); if (!this.cookie) { this.initPopupDelay(); } // Open modal if triggered by newsletter reminder document.addEventListener('reminder:openNewsletter', () => { this.modal.open(); }); } NewsletterPopup.prototype = Object.assign({}, NewsletterPopup.prototype, { initPopupDelay: function () { if (this.data.testMode === 'true') { return; } setTimeout(function () { const newsletterAppeared = (sessionStorage.getItem('newsletterAppeared') === 'true'); if (newsletterAppeared) { const openReminder = new CustomEvent('newsletter:openReminder', { bubbles: true }); this.container.dispatchEvent(openReminder); } else { this.modal.open(); sessionStorage.setItem('newsletterAppeared', true); } }.bind(this), this.data.secondsBeforeShow * 1000); }, closePopup: function (success) { // Remove a cookie in case it was set in test mode if (this.data.testMode === 'true') { Cookies.remove(this.cookieName, { path: '/' }); return; } var expiry = success ? 200 : this.data.daysBeforeReappear; var hasReminder = (this.data.hasReminder === 'true'); var reminderAppeared = (sessionStorage.getItem('reminderAppeared') === 'true'); if (hasReminder && reminderAppeared) { Cookies.set(this.cookieName, 'opened', { path: '/', expires: expiry }); } else if(!hasReminder) { Cookies.set(this.cookieName, 'opened', { path: '/', expires: expiry }); } }, onLoad: function () { this.modal.open(); }, onSelect: function () { this.modal.open(); }, onDeselect: function () { this.modal.close(); }, onBlockSelect: function () { this.modal.close(); }, onBlockDeselect: function () { this.modal.open(); }, onUnload: function() { this.modal.close(); } }); return NewsletterPopup; })(); theme.PasswordHeader = (function() { function PasswordHeader() { this.init(); } PasswordHeader.prototype = Object.assign({}, PasswordHeader.prototype, { init: function() { if (!document.querySelector('#LoginModal')) { return; } var passwordModal = new theme.Modals('LoginModal', 'login-modal', { focusIdOnOpen: 'password', solid: true }); // Open modal if errors exist if (document.querySelectorAll('.errors').length) { passwordModal.open(); } } }); return PasswordHeader; })(); theme.Photoswipe = (function() { var selectors = { trigger: '.js-photoswipe__zoom', images: '.photoswipe__image', slideshowTrack: '.flickity-viewport ', activeImage: '.is-selected' }; function Photoswipe(container, sectionId) { this.container = container; this.sectionId = sectionId; this.namespace = '.photoswipe-' + this.sectionId; this.gallery; this.images; this.items; this.inSlideshow = false; if (!container || container.dataset.zoom === 'false') { return; } this.init(); } Photoswipe.prototype = Object.assign({}, Photoswipe.prototype, { init: function() { this.container.querySelectorAll(selectors.trigger).forEach(trigger => { trigger.on('click' + this.namespace, this.triggerClick.bind(this)); }); }, triggerClick: function(evt) { // Streamline changes between a slideshow and // stacked images, so recheck if we are still // working with a slideshow when initializing zoom if (this.container.dataset && this.container.dataset.hasSlideshow === 'true') { this.inSlideshow = true; } else { this.inSlideshow = false; } this.items = this.getImageData(); var image = this.inSlideshow ? this.container.querySelector(selectors.activeImage) : evt.currentTarget; var index = this.inSlideshow ? this.getChildIndex(image) : image.dataset.index; this.initGallery(this.items, index); }, // Because of image set feature, need to get index based on location in parent getChildIndex: function(el) { var i = 0; while( (el = el.previousSibling) != null ) { i++; } // 1-based index required return i + 1; }, getImageData: function() { this.images = this.inSlideshow ? this.container.querySelectorAll(selectors.slideshowTrack + selectors.images) : this.container.querySelectorAll(selectors.images); var items = []; var options = {}; this.images.forEach(el => { var item = { msrc: el.currentSrc || el.src, src: el.getAttribute('data-photoswipe-src'), w: el.getAttribute('data-photoswipe-width'), h: el.getAttribute('data-photoswipe-height'), el: el, initialZoomLevel: 0.5 } items.push(item); }); return items; }, initGallery: function(items, index) { var pswpElement = document.querySelectorAll('.pswp')[0]; var options = { allowPanToNext: false, captionEl: false, closeOnScroll: false, counterEl: false, history: false, index: index - 1, pinchToClose: false, preloaderEl: false, scaleMode: 'zoom', shareEl: false, tapToToggleControls: false, getThumbBoundsFn: function(index) { var pageYScroll = window.pageYOffset || document.documentElement.scrollTop; var thumbnail = items[index].el; var rect = thumbnail.getBoundingClientRect(); return {x:rect.left, y:rect.top + pageYScroll, w:rect.width}; } } this.gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options); this.gallery.listen('afterChange', this.afterChange.bind(this)); this.gallery.init(); this.preventiOS15Scrolling(); }, afterChange: function() { var index = this.gallery.getCurrentIndex(); this.container.dispatchEvent(new CustomEvent('photoswipe:afterChange', { detail: { index: index } })); }, syncHeight: function() { document.documentElement.style.setProperty( "--window-inner-height", `${window.innerHeight}px` ); }, // Fix poached from https://gist.github.com/dimsemenov/0b8c255c0d87f2989e8ab876073534ea preventiOS15Scrolling: function() { let initialScrollPos; if (!/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) return; this.syncHeight(); // Store scroll position to restore it later initialScrollPos = window.scrollY; // Add class to root element when PhotoSwipe opens document.documentElement.classList.add('pswp-open-in-ios'); window.addEventListener('resize', this.syncHeight); this.gallery.listen('destroy', () => { document.documentElement.classList.remove('pswp-open-in-ios'); window.scrollTo(0, initialScrollPos); }); } }); return Photoswipe; })(); /*============================================================================ ProductRecommendations ==============================================================================*/ class ProductRecommendations extends HTMLElement { constructor() { super(); this.el = this; this.url = this.dataset.url; this.intent = this.dataset.intent; this.placeholder = this.querySelector('.product-recommendations-placeholder'); this.productResults = this.querySelector('.grid-product'); this.sectionId = this.dataset.sectionId; this.blockId = this.dataset.blockId; this.init(); document.addEventListener('shopify:section:load', (e) => { if(e.detail.sectionId === this.sectionId) { this.init(); } }); document.addEventListener('shopify:block:deselect', (e) => { if(e.detail.blockId === this.blockId) { this.init(); } }); document.addEventListener('recommendations:loaded', (e) => { this.colorImages = this.el.querySelectorAll('.grid-product__color-image'); if (this.colorImages.length) { this.swatches = this.el.querySelectorAll('.color-swatch--with-image'); this.colorSwatchHovering(); } }); } init() { fetch(this.url).then(function(response) { return response.text(); }).then(function(html) { // Convert the HTML string into a document object const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const div = doc.querySelector('.product-recommendations'); if (!div) { this.el.classList.add('hide'); if (AOS) { AOS.refreshHard() } return; } this.placeholder.innerHTML = ''; this.placeholder.appendChild(div); /* When images load in Safari they have no intrinsic size so we are forcing the images to be parsed again. Instead of checking for the Safari browser we can add a check for a 0 natural width and 0 natural height for the first image */ const firstImage = this.placeholder.querySelector('.grid-product__image-wrap img') || this.placeholder.querySelector('.grid-product__image-wrap svg'); if (firstImage && firstImage.naturalWidth === 0 && firstImage.naturalHeight === 0) { this.placeholder.querySelectorAll('img').forEach(image => image.outerHTML = image.outerHTML); } theme.reinitProductGridItem(this.el); this.slideshow = this.querySelector('[data-slideshow]'); if (this.slideshow) { this.setupSlider(); } document.dispatchEvent(new CustomEvent('recommendations:loaded', { detail: { section: this.el, intent: this.intent } })); }.bind(this)); } setupSlider() { const controlType = this.slideshow.dataset.controls; const perSlide = parseFloat(this.slideshow.dataset.perSlide); const count = parseFloat(this.slideshow.dataset.count); let prevNextButtons = false; let pageDots = true; if (controlType === 'arrows') { pageDots = false; prevNextButtons = true; } if (perSlide < count) { this.flickity = new theme.Slideshow(this.slideshow, { prevNextButtons, pageDots, adaptiveHeight: true, wrapAround: false, }); } } colorSwatchHovering() { this.swatches.forEach(swatch => { swatch.addEventListener('mouseenter', function() { this.setActiveColorImage(swatch); }.bind(this)); swatch.addEventListener('touchstart', function(evt) { evt.preventDefault(); this.setActiveColorImage(swatch); }.bind(this), {passive: true}); swatch.addEventListener('mouseleave', function() { this.removeActiveColorImage(swatch); }.bind(this)); }); } setActiveColorImage(swatch) { const id = swatch.dataset.variantId; const image = swatch.dataset.variantImage; // Unset all active swatch images this.colorImages.forEach(el => { el.classList.remove('is-active'); }); // Unset all active swatches this.swatches.forEach(el => { el.classList.remove('is-active'); }); // Set active image and swatch const imageEl = this.el.querySelector(`.grid-product__color-image--${id}`); imageEl.style.backgroundImage = 'url(' + image + ')'; imageEl.classList.add('is-active'); swatch.classList.add('is-active'); // Update product grid item href with variant URL const variantUrl = swatch.dataset.url; const gridItem = swatch.closest('.grid-item__link'); gridItem.setAttribute('href', variantUrl); } removeActiveColorImage(swatch) { const id = swatch.dataset.variantId; this.el.querySelector(`.grid-product__color-image--${id}`).classList.remove('is-active'); } } customElements.define('product-recommendations', ProductRecommendations); theme.SlideshowSection = (function() { var selectors = { parallaxContainer: '.parallax-container' }; function SlideshowSection(container) { this.container = container; var sectionId = container.getAttribute('data-section-id'); this.slideshow = container.querySelector('#Slideshow-' + sectionId); this.namespace = '.' + sectionId; this.initialIndex = 0; if (!this.slideshow) { return } // Get shopify-created div that section markup lives in, // then get index of it inside its parent var sectionEl = container.parentElement; var sectionIndex = [].indexOf.call(sectionEl.parentElement.children, sectionEl); if (sectionIndex === 0) { this.init(); } else { theme.initWhenVisible({ element: this.container, callback: this.init.bind(this) }); } } SlideshowSection.prototype = Object.assign({}, SlideshowSection.prototype, { init: function() { var slides = this.slideshow.querySelectorAll('.slideshow__slide'); this.slideshow.classList.remove('loading', 'loading--delayed'); this.slideshow.classList.add('loaded'); if (slides.length > 1) { var sliderArgs = { prevNextButtons: this.slideshow.hasAttribute('data-arrows'), pageDots: this.slideshow.hasAttribute('data-dots'), fade: true, setGallerySize: false, initialIndex: this.initialIndex, autoPlay: this.slideshow.dataset.autoplay === 'true' ? parseInt(this.slideshow.dataset.speed) : false }; this.flickity = new theme.Slideshow(this.slideshow, sliderArgs); } else { // Add loaded class to first slide slides[0].classList.add('is-selected'); } }, forceReload: function() { this.onUnload(); this.init(); }, onUnload: function() { if (this.flickity && typeof this.flickity.destroy === 'function') { this.flickity.destroy(); } }, onDeselect: function() { if (this.flickity && typeof this.flickity.play === 'function') { this.flickity.play(); } }, onBlockSelect: function(evt) { var slide = this.slideshow.querySelector('.slideshow__slide--' + evt.detail.blockId) var index = parseInt(slide.dataset.index); if (this.flickity && typeof this.flickity.pause === 'function') { this.flickity.goToSlide(index); this.flickity.pause(); } else { // If section reloads, slideshow might not have been setup yet, wait a second and try again this.initialIndex = index; setTimeout(function() { if (this.flickity && typeof this.flickity.pause === 'function') { this.flickity.pause(); } }.bind(this), 1000); } }, onBlockDeselect: function() { if (this.flickity && typeof this.flickity.play === 'function') { if (this.flickity.args.autoPlay) { this.flickity.play(); } } } }); return SlideshowSection; })(); theme.StoreAvailability = (function() { var selectors = { drawerOpenBtn: '.js-drawer-open-availability', modalOpenBtn: '.js-modal-open-availability', productTitle: '[data-availability-product-title]' }; function StoreAvailability(container) { this.container = container; this.baseUrl = container.dataset.baseUrl; this.productTitle = container.dataset.productName; } StoreAvailability.prototype = Object.assign({}, StoreAvailability.prototype, { updateContent: function(variantId) { var variantSectionUrl = this.baseUrl + '/variants/' + variantId + '/?section_id=store-availability'; var self = this; fetch(variantSectionUrl) .then(function(response) { return response.text(); }) .then(function(html) { if (html.trim() === '') { this.container.innerHTML = ''; return; } self.container.innerHTML = html; self.container.innerHTML = self.container.firstElementChild.innerHTML; // Setup drawer if have open button if (self.container.querySelector(selectors.drawerOpenBtn)) { self.drawer = new theme.Drawers('StoreAvailabilityDrawer', 'availability'); } // Setup modal if have open button if (self.container.querySelector(selectors.modalOpenBtn)) { self.modal = new theme.Modals('StoreAvailabilityModal', 'availability'); } var title = self.container.querySelector(selectors.productTitle); if (title) { title.textContent = self.productTitle; } }); } }); return StoreAvailability; })(); theme.VideoSection = (function() { var selectors = { videoParent: '.video-parent-section' }; function videoSection(container) { this.container = container; this.sectionId = container.getAttribute('data-section-id'); this.namespace = '.video-' + this.sectionId; this.videoObject; theme.initWhenVisible({ element: this.container, callback: this.init.bind(this), threshold: 500 }); } videoSection.prototype = Object.assign({}, videoSection.prototype, { init: function() { var dataDiv = this.container.querySelector('.video-div'); if (!dataDiv) { return; } var type = dataDiv.dataset.type; switch(type) { case 'youtube': var videoId = dataDiv.dataset.videoId; this.initYoutubeVideo(videoId); break; case 'vimeo': var videoId = dataDiv.dataset.videoId; this.initVimeoVideo(videoId); break; case 'mp4': this.initMp4Video(); break; } }, initYoutubeVideo: function(videoId) { this.videoObject = new theme.YouTube( 'YouTubeVideo-' + this.sectionId, { videoId: videoId, videoParent: selectors.videoParent } ); }, initVimeoVideo: function(videoId) { this.videoObject = new theme.VimeoPlayer( 'Vimeo-' + this.sectionId, videoId, { videoParent: selectors.videoParent } ); }, initMp4Video: function() { var mp4Video = 'Mp4Video-' + this.sectionId; var mp4Div = document.getElementById(mp4Video); var parent = mp4Div.closest(selectors.videoParent); if (mp4Div) { parent.classList.add('loaded'); var playPromise = document.querySelector('#' + mp4Video).play(); // Edge does not return a promise (video still plays) if (playPromise !== undefined) { playPromise.then(function() { // playback normal }).catch(function() { mp4Div.setAttribute('controls', ''); parent.classList.add('video-interactable'); }); } } }, onUnload: function(evt) { var sectionId = evt.target.id.replace('shopify-section-', ''); if (this.videoObject && typeof this.videoObject.destroy === 'function') { this.videoObject.destroy(); } } }); return videoSection; })(); /*============================================================================ CountdownTimer ==============================================================================*/ class CountdownTimer extends HTMLElement { constructor() { super(); this.el = this; this.display = this.querySelector('[data-time-display]'); this.block = this.closest('.countdown__block--timer'); this.year = this.el.dataset.year; this.month = this.el.dataset.month; this.day = this.el.dataset.day; this.hour = this.el.dataset.hour; this.minute = this.el.dataset.minute; this.daysPlaceholder = this.querySelector('[date-days-placeholder]'); this.hoursPlaceholder = this.querySelector('[date-hours-placeholder]'); this.minutesPlaceholder = this.querySelector('[date-minutes-placeholder]'); this.secondsPlaceholder = this.querySelector('[date-seconds-placeholder]'); this.messagePlaceholder = this.querySelector('[data-message-placeholder]'); this.hideTimerOnComplete = this.el.dataset.hideTimer; this.completeMessage = this.el.dataset.completeMessage; this.timerComplete = false; this.init(); } init() { setInterval(() => { if (!this.timerComplete) { this._calculate(); } }, 1000); } _calculate() { // Find time difference and convert to integer const timeDifference = +new Date(`${this.month}/${this.day}/${this.year} ${this.hour}:${this.minute}:00`).getTime() - +new Date().getTime(); // If time difference is greater than 0, calculate remaining time if (timeDifference > 0) { const intervals = { days: Math.floor(timeDifference / (1000 * 60 * 60 * 24)), hours: Math.floor((timeDifference / (1000 * 60 * 60)) % 24), minutes: Math.floor((timeDifference / 1000 / 60) % 60), seconds: Math.floor((timeDifference / 1000) % 60), }; this.daysPlaceholder.innerHTML = intervals.days; this.hoursPlaceholder.innerHTML = intervals.hours; this.minutesPlaceholder.innerHTML = intervals.minutes; this.secondsPlaceholder.innerHTML = intervals.seconds; } else { if (this.completeMessage && this.messagePlaceholder) { this.messagePlaceholder.classList.add('countdown__timer-message--visible'); } if (this.hideTimerOnComplete === 'true') { this.display.classList.remove('countdown__display--visible'); this.display.classList.add('countdown__display--hidden'); } if (!this.completeMessage && this.hideTimerOnComplete === 'true') { this.block.classList.add('countdown__block--hidden'); } this.timerComplete = true; } } } customElements.define('countdown-timer', CountdownTimer); /*============================================================================ HotSpots ==============================================================================*/ class HotSpots extends HTMLElement { constructor() { super(); this.el = this; this.buttons = this.querySelectorAll('[data-button]'); this.hotspotBlocks = this.querySelectorAll('[data-hotspot-block]'); this.blockContainer = this.querySelector('[data-block-container]'); this.colorImages = this.querySelectorAll('.grid-product__color-image'); this.colorSwatches = this.querySelectorAll('.color-swatch--with-image'); this._bindEvents(); this._setupQuickShop(); if (this.colorImages.length) { this._colorSwatchHovering(); } } _colorSwatchHovering() { this.colorSwatches.forEach(swatch => { swatch.addEventListener('mouseenter', function() { this._setActiveColorImage(swatch); }.bind(this)); swatch.addEventListener('touchstart', function(evt) { evt.preventDefault(); this._setActiveColorImage(swatch); }.bind(this), {passive: true}); swatch.addEventListener('mouseleave', function() { this._removeActiveColorImage(swatch); }.bind(this)); }); } _setActiveColorImage(swatch) { var id = swatch.dataset.variantId; var image = swatch.dataset.variantImage; // Unset all active swatch images this.colorImages.forEach(el => { el.classList.remove('is-active'); }); // Unset all active swatches this.colorSwatches.forEach(el => { el.classList.remove('is-active'); }); // Set active image and swatch var imageEl = this.el.querySelector('.grid-product__color-image--' + id); imageEl.style.backgroundImage = 'url(' + image + ')'; imageEl.classList.add('is-active'); swatch.classList.add('is-active'); // Update product grid item href with variant URL var variantUrl = swatch.dataset.url; var gridItem = swatch.closest('.grid-item__link'); if (gridItem) gridItem.setAttribute('href', variantUrl); } _removeActiveColorImage(swatch) { const id = swatch.dataset.variantId; this.querySelector(`.grid-product__color-image--${id}`).classList.remove('is-active'); } /* Setup event listeners */ _bindEvents() { this.buttons.forEach(button => { const id = button.dataset.button; button.on('click', e => { e.preventDefault(); e.stopPropagation(); this._showContent(id); }); }); // Display active hotspot block on theme editor select document.addEventListener('shopify:block:select', (e) => { const blockId = e.detail.blockId; this._showContent(`${blockId}`); this._setupQuickShop(); }); } /* Toggle sidebar content */ _showContent(id) { // Hide all hotspotBlock // Show the hotspotBlock with the id this.hotspotBlocks.forEach((block) => { if (block.dataset.hotspotBlock === id) { block.classList.add('is-active'); } else { block.classList.remove('is-active'); } }); } _setupQuickShop() { if (this.querySelectorAll('[data-block-type="product"]').length > 0) { // Ensure we are utilizing the right version of QuickShop based off of theme if (typeof theme.QuickShop === 'function') { new theme.QuickShop(this.blockContainer); } else if (typeof theme.initQuickShop === 'function') { theme.initQuickShop(); } if (typeof theme.QuickAdd === 'function') { new theme.QuickAdd(this.blockContainer); } } } } customElements.define('hot-spots', HotSpots); /*============================================================================ ImageCompare ==============================================================================*/ class ImageCompare extends HTMLElement { constructor() { super(); this.el = this; this.sectionId = this.dataset.sectionId; this.button = this.querySelector('[data-button]'); this.draggableContainer = this.querySelector('[data-draggable]'); this.primaryImage = this.querySelector('[data-primary-image]'); this.secondaryImage = this.querySelector('[data-secondary-image]'); this.calculateSizes(); this.active = false; this.currentX = 0; this.initialX = 0; this.xOffset = 0; this.buttonOffset = this.button.offsetWidth / 2; this.el.addEventListener("touchstart", this.dragStart, false); this.el.addEventListener("touchend", this.dragEnd, false); this.el.addEventListener("touchmove", this.drag, false); this.el.addEventListener("mousedown", this.dragStart, false); this.el.addEventListener("mouseup", this.dragEnd, false); this.el.addEventListener("mousemove", this.drag, false); window.on('resize', theme.utils.debounce(250, () => { this.calculateSizes(true)})); document.addEventListener('shopify:section:load', event => { if (event.detail.sectionId === this.sectionId && this.primaryImage !== null) { this.calculateSizes(); } }); } calculateSizes(hasResized = false) { this.active = false; this.currentX = 0; this.initialX = 0; this.xOffset = 0; this.buttonOffset = this.button.offsetWidth / 2; this.elWidth = this.el.offsetWidth; this.button.style.transform = `translate(-${this.buttonOffset}px, -50%)`; this.primaryImage.style.width = `${this.elWidth}px`; if (hasResized) this.draggableContainer.style.width = `${this.elWidth/2}px`; } dragStart(e) { if (e.type === "touchstart") { this.initialX = e.touches[0].clientX - this.xOffset; } else { this.initialX = e.clientX - this.xOffset; } if (e.target === this.button) { this.active = true; } } dragEnd(e) { this.initialX = this.currentX; this.active = false; } drag(e) { if (this.active) { e.preventDefault(); if (e.type === "touchmove") { this.currentX = e.touches[0].clientX - this.initialX; } else { this.currentX = e.clientX - this.initialX; } this.xOffset = this.currentX; this.setTranslate(this.currentX, this.button); } } setTranslate(xPos, el) { let newXpos = xPos - this.buttonOffset; let newVal = (this.elWidth/2) + xPos; const boundaryPadding = 50; const XposMin = (this.elWidth/2 + this.buttonOffset) * -1; const XposMax = this.elWidth/2 - this.buttonOffset; // Set boundaries for dragging if (newXpos < (XposMin + boundaryPadding)) { newXpos = XposMin + boundaryPadding; newVal = boundaryPadding } else if (newXpos > (XposMax - boundaryPadding)) { newXpos = XposMax - boundaryPadding; newVal = this.elWidth - boundaryPadding; } el.style.transform = `translate(${newXpos}px, -50%)`; this.draggableContainer.style.width = `${newVal}px`; } } customElements.define('image-compare', ImageCompare); theme.Blog = (function() { function Blog(container) { this.tagFilters(); } Blog.prototype = Object.assign({}, Blog.prototype, { tagFilters: function() { var filterBy = document.getElementById('BlogTagFilter'); if (!filterBy) { return; } filterBy.addEventListener('change', function() { location.href = filterBy.value; }); } }); return Blog; })(); theme.CollectionHeader = (function() { var hasLoadedBefore = false; function CollectionHeader(container) { this.namespace = '.collection-header'; var heroImageContainer = container.querySelector('.collection-hero'); if (heroImageContainer) { if (hasLoadedBefore) { this.checkIfNeedReload(); } heroImageContainer.classList.remove('loading', 'loading--delayed'); heroImageContainer.classList.add('loaded'); } else if (theme.settings.overlayHeader) { theme.headerNav.disableOverlayHeader(); } hasLoadedBefore = true; } CollectionHeader.prototype = Object.assign({}, CollectionHeader.prototype, { // A liquid variable in the header needs a full page refresh // if the collection header hero image setting is enabled // and the header is set to sticky. Only necessary in the editor. checkIfNeedReload: function() { if (!Shopify.designMode) { return; } if (theme.settings.overlayHeader) { var header = document.querySelector('.header-wrapper'); if (!header.classList.contains('header-wrapper--overlay')) { location.reload(); } } } }); return CollectionHeader; })(); theme.CollectionSidebar = (function() { var drawerStyle = false; var selectors = { sidebar: '#CollectionSidebar', }; function CollectionSidebar(container) { this.container = container.querySelector(selectors.sidebar); } CollectionSidebar.prototype = Object.assign({}, CollectionSidebar.prototype, { init: function() { // Do not load when no sidebar exists if(!this.container) { return; } this.onUnload(); drawerStyle = this.container.dataset.style === 'drawer'; theme.FilterDrawer = new theme.Drawers('FilterDrawer', 'collection-filters', true); }, forceReload: function() { this.init(); }, onSelect: function() { if (theme.FilterDrawer) { if (!drawerStyle) { theme.FilterDrawer.close(); return; } if (drawerStyle || theme.config.bpSmall) { theme.FilterDrawer.open(); } } }, onDeselect: function() { if (theme.FilterDrawer) { theme.FilterDrawer.close(); } }, onUnload: function() { if (theme.FilterDrawer) { theme.FilterDrawer.close(); } } }); return CollectionSidebar; })(); theme.Collection = (function() { var isAnimating = false; var selectors = { sortSelect: '#SortBy', colorSwatchImage: '.grid-product__color-image', colorSwatch: '.color-swatch--with-image', collectionGrid: '.collection-grid__wrapper', trigger: '.collapsible-trigger', sidebar: '#CollectionSidebar', filterSidebar: '.collapsible-content--sidebar', activeTagList: '.tag-list--active-tags', tags: '.tag-list input', activeTags: '.tag-list a', tagsForm: '.filter-form', filters: '.collection-filter', priceRange: '.price-range', }; var classes = { activeTag: 'tag--active', removeTagParent: 'tag--remove', filterSidebar: 'collapsible-content--sidebar', isOpen: 'is-open', }; function Collection(container) { this.container = container; this.sectionId = container.getAttribute('data-section-id'); this.namespace = '.collection-' + this.sectionId; this.sidebar = new theme.CollectionSidebar(container); this.context = container.getAttribute('data-context'); this.ajaxRenderer = new theme.AjaxRenderer({ sections: [ { sectionId: this.sectionId, nodeId: 'CollectionAjaxContent', }, ], onReplace: this.onReplaceAjaxContent.bind(this), }); this.init(); } Collection.prototype = Object.assign({}, Collection.prototype, { init: function() { /* Prevent extra JS from running for featured collection on a collection template */ if (this.context && this.context === 'featured-collection') { this.colorSwatchHovering(); } else { this.initSort(); this.colorSwatchHovering(); this.initFilters(); this.initPriceRange(); this.sidebar.init(); } }, initSort: function() { this.sortSelect = document.querySelector(selectors.sortSelect); if (this.sortSelect) { this.defaultSort = this.getDefaultSortValue(); this.sortSelect.on('change' + this.namespace, this.onSortChange.bind(this)); } }, getSortValue: function() { return this.sortSelect.value || this.defaultSort; }, getDefaultSortValue: function() { return this.sortSelect.getAttribute('data-default-sortby'); }, onSortChange: function() { this.queryParams = new URLSearchParams(window.location.search); this.queryParams.set('sort_by', this.getSortValue()); this.queryParams.delete('page'); // Delete if it exists window.location.search = this.queryParams.toString(); }, colorSwatchHovering: function() { var colorImages = this.container.querySelectorAll(selectors.colorSwatchImage); if (!colorImages.length) { return; } this.container.querySelectorAll(selectors.colorSwatch).forEach(swatch => { swatch.addEventListener('mouseenter', () => { var id = swatch.dataset.variantId; var image = swatch.dataset.variantImage; var el = this.container.querySelector('.grid-product__color-image--' + id); el.style.backgroundImage = 'url(' + image + ')'; el.classList.add('is-active'); }); swatch.addEventListener('mouseleave', () => { var id = swatch.dataset.variantId; this.container.querySelector('.grid-product__color-image--' + id).classList.remove('is-active'); }); }); }, /*==================== Collection filters ====================*/ initFilters: function() { var tags = document.querySelectorAll(selectors.tags); if (!tags.length) { return; } this.bindBackButton(); // Set mobile top value for filters if sticky header enabled if (theme.config.stickyHeader) { this.setFilterStickyPosition(); window.on('resize', theme.utils.debounce(500, this.setFilterStickyPosition)); } document.querySelectorAll(selectors.activeTags).forEach(tag => { tag.addEventListener('click', this.tagClick.bind(this)); }); document.querySelectorAll(selectors.tagsForm).forEach(form => { form.addEventListener('input', this.onFormSubmit.bind(this)); }); }, initPriceRange: function() { const priceRangeEls = document.querySelectorAll(selectors.priceRange); priceRangeEls.forEach((el) => new theme.PriceRange(el, { // onChange passes in formData onChange: this.renderFromFormData.bind(this), })); }, tagClick: function(evt) { var el = evt.currentTarget; if (theme.FilterDrawer) { theme.FilterDrawer.close(); } // Do not ajax-load collection links if (el.classList.contains('no-ajax')) { return; } evt.preventDefault(); if (isAnimating) { return; } isAnimating = true; const parent = el.parentNode; const newUrl = new URL(el.href); this.renderActiveTag(parent, el); this.updateScroll(true); this.startLoading(); this.renderCollectionPage(newUrl.searchParams); }, onFormSubmit: function(evt) { var el = evt.target; if (theme.FilterDrawer) { theme.FilterDrawer.close(); } // Do not ajax-load collection links if (el.classList.contains('no-ajax')) { return; } evt.preventDefault(); if (isAnimating) { return; } isAnimating = true; const parent = el.closest('li'); const formEl = el.closest('form'); const formData = new FormData(formEl); this.renderActiveTag(parent, el); this.updateScroll(true); this.startLoading(); this.renderFromFormData(formData); }, fetchOpenCollasibleFilters: function() { return Array.from( document.querySelectorAll( `${selectors.sidebar} ${selectors.trigger}.${classes.isOpen}`, ), ).map(trigger => trigger.dataset.collapsibleId); }, renderActiveTag: function(parent, el) { const textEl = parent.querySelector('.tag__text'); if (parent.classList.contains(classes.activeTag)) { parent.classList.remove(classes.activeTag); } else { parent.classList.add(classes.activeTag); // If adding a tag, show new tag right away. // Otherwise, remove it before ajax finishes if (el.closest('li').classList.contains(classes.removeTagParent)) { parent.remove(); } else { // Append new tag in both drawer and sidebar document.querySelectorAll(selectors.activeTagList).forEach(list => { const newTag = document.createElement('li'); const newTagLink = document.createElement('a'); newTag.classList.add('tag', 'tag--remove'); newTagLink.classList.add('btn', 'btn--small'); newTagLink.innerText = textEl.innerText; newTag.appendChild(newTagLink); list.appendChild(newTag); }); } } }, renderFromFormData: function(formData) { const searchParams = new URLSearchParams(formData); this.renderCollectionPage(searchParams); }, onReplaceAjaxContent: function(newDom, section) { const openCollapsibleIds = this.fetchOpenCollasibleFilters(); openCollapsibleIds.forEach(selector => { newDom .querySelectorAll(`[data-collapsible-id=${selector}]`) .forEach(this.openCollapsible); }); var newContentEl = newDom.getElementById(section.nodeId); if (!newContentEl) { return; } document.getElementById(section.nodeId).innerHTML = newContentEl.innerHTML; }, openCollapsible: function(el) { if (el.classList.contains(classes.filterSidebar)) { el.style.height = 'auto'; } el.classList.add(classes.isOpen); }, renderCollectionPage: function(searchParams, updateURLHash = true) { this.ajaxRenderer .renderPage(window.location.pathname, searchParams, updateURLHash) .then(() => { theme.sections.reinit('collection-grid'); this.updateScroll(false); this.initPriceRange(); theme.reinitProductGridItem(); document.dispatchEvent(new CustomEvent('collection:reloaded')); isAnimating = false; }); }, bindBackButton: function() { // Ajax page on back button window.off('popstate' + this.namespace); window.on('popstate' + this.namespace, function(state) { if (state) { const newUrl = new URL(window.location.href); this.renderCollectionPage(newUrl.searchParams, false); } }.bind(this)); }, updateScroll: function(animate) { var scrollToElement = document.querySelector('[data-scroll-to]'); var scrollTo = scrollToElement && scrollToElement.offsetTop; if (!theme.config.bpSmall) { scrollTo -= 15; } if (theme.config.stickyHeader) { var headerHeight = document.querySelector('.site-header').offsetHeight; scrollTo = scrollTo - headerHeight; } if (animate) { window.scrollTo({top: scrollTo, behavior: 'smooth'}); } else { window.scrollTo({top: scrollTo}); } }, setFilterStickyPosition: function() { var headerHeight = document.querySelector('.site-header').offsetHeight; document.querySelector(selectors.filters).style.top = headerHeight + 10 + 'px'; // Also update top position of sticky sidebar var stickySidebar = document.querySelector('.grid__item--sidebar'); if (stickySidebar) { stickySidebar.style.top = headerHeight + 10 + 'px'; } }, forceReload: function() { this.init(); }, startLoading: function() { document.querySelector(selectors.collectionGrid).classList.add('unload'); }, }); return Collection; })(); theme.FooterSection = (function() { var selectors = { locale: '[data-disclosure-locale]', currency: '[data-disclosure-currency]' }; function FooterSection(container) { this.container = container; this.localeDisclosure = null; this.currencyDisclosure = null; this.init(); } FooterSection.prototype = Object.assign({}, FooterSection.prototype, { init: function() { var localeEl = this.container.querySelector(selectors.locale); var currencyEl = this.container.querySelector(selectors.currency); if (localeEl) { this.localeDisclosure = new theme.Disclosure(localeEl); } if (currencyEl) { this.currencyDisclosure = new theme.Disclosure(currencyEl); } // Change email icon to submit text var newsletterInput = document.querySelector('.footer__newsletter-input'); if (newsletterInput) { newsletterInput.addEventListener('keyup', function() { newsletterInput.classList.add('footer__newsletter-input--active'); }); } // Re-hook up collapsible box triggers theme.collapsibles.init(this.container); }, onUnload: function() { if (this.localeDisclosure) { this.localeDisclosure.destroy(); } if (this.currencyDisclosure) { this.currencyDisclosure.destroy(); } } }); return FooterSection; })(); theme.HeaderSection = (function() { var selectors = { locale: '[data-disclosure-locale]', currency: '[data-disclosure-currency]' }; function HeaderSection(container) { this.container = container; this.sectionId = this.container.getAttribute('data-section-id'); this.init(); } HeaderSection.prototype = Object.assign({}, HeaderSection.prototype, { init: function() { // Reload any slideshow if header is reloaded to make sure // sticky header works as expected // (can be anywhere in sections.instance array) if (Shopify && Shopify.designMode) { theme.sections.reinit('slideshow-section'); // Set a timer to resize the header in case the logo changes size setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 500); } this.initDrawers(); this.initDisclosures(); theme.headerNav.init(); theme.announcementBar.init(); }, initDisclosures: function() { var localeEl = this.container.querySelector(selectors.locale); var currencyEl = this.container.querySelector(selectors.currency); if (localeEl) { this.localeDisclosure = new theme.Disclosure(localeEl); } if (currencyEl) { this.currencyDisclosure = new theme.Disclosure(currencyEl); } }, initDrawers: function() { theme.NavDrawer = new theme.Drawers('NavDrawer', 'nav'); if (theme.settings.cartType === 'drawer') { if (!document.body.classList.contains('template-cart')) { new theme.CartDrawer(); } } theme.collapsibles.init(document.getElementById('NavDrawer')); }, onBlockSelect: function(evt) { theme.announcementBar.onBlockSelect(evt.detail.blockId); }, onBlockDeselect: function() { theme.announcementBar.onBlockDeselect(); }, onUnload: function() { theme.NavDrawer.close(); theme.announcementBar.unload(); if (this.localeDisclosure) { this.localeDisclosure.destroy(); } if (this.currencyDisclosure) { this.currencyDisclosure.destroy(); } } }); return HeaderSection; })(); theme.Product = (function() { var videoObjects = {}; var classes = { onSale: 'on-sale', disabled: 'disabled', isModal: 'is-modal', loading: 'loading', loaded: 'loaded', hidden: 'hide', interactable: 'video-interactable', visuallyHide: 'visually-invisible' }; var selectors = { productVideo: '.product__video', videoParent: '.product__video-wrapper', slide: '.product-main-slide', currentSlide: '.is-selected', startingSlide: '.starting-slide', variantType: '.variant-wrapper', blocks: '[data-product-blocks]', blocksHolder: '[data-blocks-holder]', dynamicVariantsEnabled: '[data-dynamic-variants-enabled]', }; function Product(container) { this.container = container; var sectionId = this.sectionId = container.getAttribute('data-section-id'); var productId = this.productId = container.getAttribute('data-product-id'); this.inModal = (container.dataset.modal === 'true'); this.modal; this.settings = { enableHistoryState: container.dataset.history || false, namespace: '.product-' + sectionId, inventory: false, inventoryThreshold: 10, modalInit: false, hasImages: true, imageSetName: null, imageSetIndex: null, currentImageSet: null, imageSize: '620x', currentSlideIndex: 0, videoLooping: container.dataset.videoLooping }; // Overwrite some settings when loaded in modal if (this.inModal) { this.settings.enableHistoryState = false; this.settings.namespace = '.product-' + sectionId + '-modal'; this.modal = document.getElementById('QuickShopModal-' + productId); } this.selectors = { variantsJson: '[data-variant-json]', currentVariantJson: '[data-current-variant-json]', form: '.product-single__form', media: '[data-product-media-type-model]', closeMedia: '.product-single__close-media', photoThumbs: '[data-product-thumb]', thumbSlider: '[data-product-thumbs]', thumbScroller: '.product__thumbs--scroller', mainSlider: '[data-product-photos]', imageContainer: '[data-product-images]', productImageMain: '[data-product-image-main]', priceWrapper: '[data-product-price-wrap]', price: '[data-product-price]', comparePrice: '[data-compare-price]', savePrice: '[data-save-price]', priceA11y: '[data-a11y-price]', comparePriceA11y: '[data-compare-price-a11y]', unitWrapper: '[data-unit-price-wrapper]', unitPrice: '[data-unit-price]', unitPriceBaseUnit: '[data-unit-base]', sku: '[data-sku]', inventory: '[data-product-inventory]', incomingInventory: '[data-incoming-inventory]', colorLabel: '[data-variant-color-label]', addToCart: '[data-add-to-cart]', addToCartText: '[data-add-to-cart-text]', originalSelectorId: '[data-product-select]', singleOptionSelector: '[data-variant-input]', variantColorSwatch: '.variant__input--color-swatch', dynamicVariantsEnabled: '[data-dynamic-variants-enabled]', availabilityContainer: '[data-store-availability-holder]' }; this.cacheElements(); this.firstProductImage = this.cache.mainSlider.querySelector('img'); if (!this.firstProductImage) { this.settings.hasImages = false; } var dataSetEl = this.cache.mainSlider.querySelector('[data-set-name]'); if (dataSetEl) { this.settings.imageSetName = dataSetEl.dataset.setName; } this.init(); } Product.prototype = Object.assign({}, Product.prototype, { init: function() { if (this.inModal) { this.container.classList.add(classes.isModal); document.addEventListener('modalOpen.QuickShopModal-' + this.productId, this.openModalProduct.bind(this)); document.addEventListener('modalClose.QuickShopModal-' + this.productId, this.closeModalProduct.bind(this)); } if (!this.inModal) { this.formSetup(); this.productSetup(); this.videoSetup(); this.initProductSlider(); this.customMediaListners(); this.addIdToRecentlyViewed(); } }, cacheElements: function() { this.cache = { form: this.container.querySelector(this.selectors.form), mainSlider: this.container.querySelector(this.selectors.mainSlider), thumbSlider: this.container.querySelector(this.selectors.thumbSlider), thumbScroller: this.container.querySelector(this.selectors.thumbScroller), productImageMain: this.container.querySelector(this.selectors.productImageMain), // Price-related priceWrapper: this.container.querySelector(this.selectors.priceWrapper), comparePriceA11y: this.container.querySelector(this.selectors.comparePriceA11y), comparePrice: this.container.querySelector(this.selectors.comparePrice), price: this.container.querySelector(this.selectors.price), savePrice: this.container.querySelector(this.selectors.savePrice), priceA11y: this.container.querySelector(this.selectors.priceA11y) }; }, formSetup: function() { this.initQtySelector(); this.initAjaxProductForm(); this.availabilitySetup(); this.initVariants(); // We know the current variant now so setup image sets if (this.settings.imageSetName) { this.updateImageSet(); } }, availabilitySetup: function() { var container = this.container.querySelector(this.selectors.availabilityContainer); if (container) { this.storeAvailability = new theme.StoreAvailability(container); } }, productSetup: function() { this.setImageSizes(); this.initImageZoom(); this.initModelViewerLibraries(); this.initShopifyXrLaunch(); if (window.SPR) {SPR.initDomEls();SPR.loadBadges()} }, setImageSizes: function() { if (!this.settings.hasImages) { return; } // Get srcset image src, works on most modern browsers // otherwise defaults to settings.imageSize var currentImage = this.firstProductImage.currentSrc; if (currentImage) { this.settings.imageSize = theme.Images.imageSize(currentImage); } }, addIdToRecentlyViewed: function() { var handle = this.container.getAttribute('data-product-handle'); var url = this.container.getAttribute('data-product-url'); var aspectRatio = this.container.getAttribute('data-aspect-ratio'); var featuredImage = this.container.getAttribute('data-img-url'); // Remove current product if already in set of recent if (theme.recentlyViewed.recent.hasOwnProperty(handle)) { delete theme.recentlyViewed.recent[handle]; } // Add it back to the end theme.recentlyViewed.recent[handle] = { url: url, aspectRatio: aspectRatio, featuredImage: featuredImage }; if (theme.config.hasLocalStorage) { window.localStorage.setItem('theme-recent', JSON.stringify(theme.recentlyViewed.recent)); } }, initVariants: function() { var variantJson = this.container.querySelector(this.selectors.variantsJson); if (!variantJson) { return; } this.variantsObject = JSON.parse(variantJson.innerHTML); var dynamicVariantsEnabled = !!this.container.querySelector(selectors.dynamicVariantsEnabled) var options = { container: this.container, enableHistoryState: this.settings.enableHistoryState, singleOptionSelector: this.selectors.singleOptionSelector, originalSelectorId: this.selectors.originalSelectorId, variants: this.variantsObject, dynamicVariantsEnabled }; var swatches = this.container.querySelectorAll(this.selectors.variantColorSwatch); if (swatches.length) { swatches.forEach(swatch => { swatch.addEventListener('change', function(evt) { var color = swatch.dataset.colorName; var index = swatch.dataset.colorIndex; this.updateColorName(color, index); }.bind(this)) }); } this.variants = new theme.Variants(options); // Product availability on page load if (this.storeAvailability) { var variant_id = this.variants.currentVariant ? this.variants.currentVariant.id : this.variants.variants[0].id; this.storeAvailability.updateContent(variant_id); this.container.on('variantChange' + this.settings.namespace, this.updateAvailability.bind(this)); } this.container.on('variantChange' + this.settings.namespace, this.updateCartButton.bind(this)); this.container.on('variantImageChange' + this.settings.namespace, this.updateVariantImage.bind(this)); this.container.on('variantPriceChange' + this.settings.namespace, this.updatePrice.bind(this)); this.container.on('variantUnitPriceChange' + this.settings.namespace, this.updateUnitPrice.bind(this)); if (this.container.querySelector(this.selectors.sku)) { this.container.on('variantSKUChange' + this.settings.namespace, this.updateSku.bind(this)); } var inventoryEl = this.container.querySelector(this.selectors.inventory); if (inventoryEl) { this.settings.inventory = true; this.settings.inventoryThreshold = inventoryEl.dataset.threshold; this.container.on('variantChange' + this.settings.namespace, this.updateInventory.bind(this)); } // Update individual variant availability on each selection if (dynamicVariantsEnabled) { var currentVariantJson = this.container.querySelector(this.selectors.currentVariantJson); if (currentVariantJson) { var variantType = this.container.querySelector(selectors.variantType); if (variantType) { new theme.VariantAvailability({ container: this.container, namespace: this.settings.namespace, type: variantType.dataset.type, variantsObject: this.variantsObject, currentVariantObject: JSON.parse(currentVariantJson.innerHTML) }); } } } // image set names variant change listeners if (this.settings.imageSetName) { var variantWrapper = this.container.querySelector('.variant-input-wrap[data-handle="'+this.settings.imageSetName+'"]'); if (variantWrapper) { this.settings.imageSetIndex = variantWrapper.dataset.index; this.container.on('variantChange' + this.settings.namespace, this.updateImageSet.bind(this)) } else { this.settings.imageSetName = null; } } }, initQtySelector: function() { this.container.querySelectorAll('.js-qty__wrapper').forEach(el => { new theme.QtySelector(el, { namespace: '.product' }); }); }, initAjaxProductForm: function() { if (theme.settings.cartType === 'drawer') { new theme.AjaxProduct(this.cache.form); } }, /*============================================================================ Variant change methods ==============================================================================*/ updateColorName: function(color, index) { // Updates on radio button change, not variant.js this.container.querySelector(this.selectors.colorLabel + `[data-index="${index}"`).textContent = color; }, updateCartButton: function(evt) { var variant = evt.detail.variant; var cartBtn = this.container.querySelector(this.selectors.addToCart); var cartBtnText = this.container.querySelector(this.selectors.addToCartText); if (!cartBtn) return; if (variant) { if (variant.available) { // Available, enable the submit button and change text cartBtn.classList.remove(classes.disabled); cartBtn.disabled = false; var defaultText = cartBtnText.dataset.defaultText; cartBtnText.textContent = defaultText; } else { // Sold out, disable the submit button and change text cartBtn.classList.add(classes.disabled); cartBtn.disabled = true; cartBtnText.textContent = theme.strings.soldOut; } } else { // The variant doesn't exist, disable submit button cartBtn.classList.add(classes.disabled); cartBtn.disabled = true; cartBtnText.textContent = theme.strings.unavailable; } }, updatePrice: function(evt) { var variant = evt.detail.variant; if (variant) { // Regular price this.cache.price.innerHTML = theme.Currency.formatMoney(variant.price, theme.settings.moneyFormat); // Sale price, if necessary if (variant.compare_at_price > variant.price) { this.cache.comparePrice.innerHTML = theme.Currency.formatMoney(variant.compare_at_price, theme.settings.moneyFormat); this.cache.priceWrapper.classList.remove(classes.hidden); this.cache.price.classList.add(classes.onSale); this.cache.comparePriceA11y.setAttribute('aria-hidden', 'false'); this.cache.priceA11y.setAttribute('aria-hidden', 'false'); var savings = variant.compare_at_price - variant.price; if (theme.settings.saveType == 'percent') { savings = Math.round(((savings) * 100) / variant.compare_at_price) + '%'; } else { savings = theme.Currency.formatMoney(savings, theme.settings.moneyFormat); } this.cache.savePrice.classList.remove(classes.hidden); this.cache.savePrice.innerHTML = theme.strings.savePrice.replace('[saved_amount]', savings); } else { if (this.cache.priceWrapper) { this.cache.priceWrapper.classList.add(classes.hidden); } this.cache.savePrice.classList.add(classes.hidden); this.cache.price.classList.remove(classes.onSale); if (this.cache.comparePriceA11y) { this.cache.comparePriceA11y.setAttribute('aria-hidden', 'true'); } this.cache.priceA11y.setAttribute('aria-hidden', 'true'); } } }, updateUnitPrice: function(evt) { var variant = evt.detail.variant; if (variant && variant.unit_price) { this.container.querySelector(this.selectors.unitPrice).innerHTML = theme.Currency.formatMoney(variant.unit_price, theme.settings.moneyFormat); this.container.querySelector(this.selectors.unitPriceBaseUnit).innerHTML = theme.Currency.getBaseUnit(variant); this.container.querySelector(this.selectors.unitWrapper).classList.remove(classes.hidden); } else { this.container.querySelector(this.selectors.unitWrapper).classList.add(classes.hidden); } }, imageSetArguments: function(variant) { var variant = variant ? variant : (this.variants ? this.variants.currentVariant : null); if (!variant) return; var setValue = this.settings.currentImageSet = this.getImageSetName(variant[this.settings.imageSetIndex]); var set = this.settings.imageSetName + '_' + setValue; // Always start on index 0 this.settings.currentSlideIndex = 0; // Return object that adds cellSelector to mainSliderArgs return { cellSelector: '[data-group="'+set+'"]', imageSet: set, initialIndex: this.settings.currentSlideIndex } }, updateImageSet: function(evt) { // If called directly, use current variant var variant = evt ? evt.detail.variant : (this.variants ? this.variants.currentVariant : null); if (!variant) { return; } var setValue = this.getImageSetName(variant[this.settings.imageSetIndex]); // Already on the current image group if (this.settings.currentImageSet === setValue) { return; } this.initProductSlider(variant); }, // Show/hide thumbnails based on current image set updateImageSetThumbs: function(set) { this.cache.thumbSlider.querySelectorAll('.product__thumb-item').forEach(thumb => { thumb.classList.toggle(classes.hidden, thumb.dataset.group !== set); }); }, getImageSetName: function(string) { return string.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, '').replace(/^-/, ''); }, updateSku: function(evt) { var variant = evt.detail.variant; var newSku = ''; if (variant) { if (variant.sku) { newSku = variant.sku; } this.container.querySelector(this.selectors.sku).textContent = newSku; } }, updateInventory: function(evt) { var variant = evt.detail.variant; // Hide stock if no inventory management or policy is continue if (!variant || !variant.inventory_management || variant.inventory_policy === 'continue') { this.toggleInventoryQuantity(variant, false); this.toggleIncomingInventory(false); return; } if (variant.inventory_management === 'shopify' && window.inventories && window.inventories[this.productId]) { var variantInventoryObject = window.inventories[this.productId][variant.id]; var quantity = variantInventoryObject.quantity; var showInventory = true; var showIncomingInventory = false; if (quantity <= 0 || quantity > this.settings.inventoryThreshold || variantInventoryObject.policy === 'continue') { showInventory = false; } this.toggleInventoryQuantity(variant, variantInventoryObject); // Only show incoming inventory when: // - inventory notice itself is hidden // - have incoming inventory // - current quantity is below theme setting threshold if (showInventory && variantInventoryObject.incoming === 'true' && quantity <= this.settings.inventoryThreshold) { showIncomingInventory = true; } this.toggleIncomingInventory(showIncomingInventory, variant.available, variantInventoryObject.next_incoming_date); } }, updateAvailability: function(evt) { var variant = evt.detail.variant; if (!variant) { return; } this.storeAvailability.updateContent(variant.id); }, toggleInventoryQuantity: function(variant, variantInventoryObject) { const { quantity, policy } = variantInventoryObject || {}; if (!this.settings.inventory) { return; } var el = this.container.querySelector(this.selectors.inventory); var salesPoint = el.closest('.product-block'); if (parseInt(quantity) <= parseInt(this.settings.inventoryThreshold) && policy !== 'continue') { el.parentNode.classList.add('inventory--low'); if (quantity > 1) { el.textContent = theme.strings.otherStockLabel.replace('[count]', quantity); } else { el.textContent = theme.strings.oneStockLabel.replace('[count]', quantity); } } else { el.parentNode.classList.remove('inventory--low'); el.textContent = theme.strings.inStockLabel; } if (variant && variant.available) { el.parentNode.classList.remove(classes.hidden); if (salesPoint) { salesPoint.classList.remove(classes.hidden); } } else { el.parentNode.classList.add(classes.hidden); if (salesPoint) { salesPoint.classList.add(classes.hidden); } } }, toggleIncomingInventory: function(show, available, date) { var el = this.container.querySelector(this.selectors.incomingInventory); if (!el) { return; } var salesPoint = el.closest('.product-block'); var textEl = el.querySelector('.js-incoming-text'); if (show) { var string = available ? theme.strings.willNotShipUntil.replace('[date]', date) : theme.strings.willBeInStockAfter.replace('[date]', date); if (!date) { string = theme.strings.waitingForStock; } el.classList.remove(classes.hidden); if (salesPoint) { salesPoint.classList.remove(classes.hidden); } textEl.textContent = string; } else { el.classList.add(classes.hidden); } }, /*============================================================================ Product videos ==============================================================================*/ videoSetup: function() { var productVideos = this.cache.mainSlider.querySelectorAll(selectors.productVideo); if (!productVideos.length) { return false; } productVideos.forEach(vid => { var type = vid.dataset.videoType; if (type === 'youtube') { this.initYoutubeVideo(vid); } else if (type === 'vimeo') { this.initVimeoVideo(vid); } else if (type === 'mp4') { this.initMp4Video(vid); } }); }, initYoutubeVideo: function(div) { videoObjects[div.id] = new theme.YouTube( div.id, { videoId: div.dataset.videoId, videoParent: selectors.videoParent, autoplay: false, // will handle this in callback style: div.dataset.videoStyle, loop: div.dataset.videoLoop, events: { onReady: this.youtubePlayerReady.bind(this), onStateChange: this.youtubePlayerStateChange.bind(this) } } ); }, initVimeoVideo: function(div) { videoObjects[div.id] = new theme.VimeoPlayer( div.id, div.dataset.videoId, { videoParent: selectors.videoParent, autoplay: false, style: div.dataset.videoStyle, loop: div.dataset.videoLoop, } ) }, // Comes from YouTube SDK // Get iframe ID with evt.target.getIframe().id // Then access product video players with videoObjects[id] youtubePlayerReady: function(evt) { var iframeId = evt.target.getIframe().id; if (!videoObjects[iframeId]) { // No youtube player data return; } var obj = videoObjects[iframeId]; var player = obj.videoPlayer; if (obj.options.style !== 'sound') { player.mute(); } obj.parent.classList.remove('loading'); obj.parent.classList.add('loaded'); obj.parent.classList.add('video-interactable'); // Previously, video was only interactable after slide change // If we have an element, it is in the visible/first slide, // and is muted, play it if (this._isFirstSlide(iframeId) && obj.options.style !== 'sound') { player.playVideo(); } }, _isFirstSlide: function(id) { return this.cache.mainSlider.querySelector(selectors.startingSlide + ' ' + '#' + id); }, youtubePlayerStateChange: function(evt) { var iframeId = evt.target.getIframe().id; var obj = videoObjects[iframeId]; switch (evt.data) { case -1: // unstarted // Handle low power state on iOS by checking if // video is reset to unplayed after attempting to buffer if (obj.attemptedToPlay) { obj.parent.classList.add('video-interactable'); } break; case 0: // ended if (obj && obj.options.loop === 'true') { obj.videoPlayer.playVideo(); } break; case 3: // buffering obj.attemptedToPlay = true; break; } }, initMp4Video: function(div) { videoObjects[div.id] = { id: div.id, type: 'mp4' }; if (this._isFirstSlide(div.id)) { this.playMp4Video(div.id); } }, stopVideos: function() { for (var [id, vid] of Object.entries(videoObjects)) { if (vid.videoPlayer) { if (typeof vid.videoPlayer.stopVideo === 'function') { vid.videoPlayer.stopVideo(); // YouTube player } } else if (vid.type === 'mp4') { this.stopMp4Video(vid.id); // MP4 player } } }, _getVideoType: function(video) { return video.getAttribute('data-video-type'); }, _getVideoDivId: function(video) { return video.id; }, playMp4Video: function(id) { var player = this.container.querySelector('#' + id); var playPromise = player.play(); if (playPromise !== undefined) { playPromise.then(function() { // Playing as expected }) .catch(function(error) { // Likely low power mode on iOS, show controls player.setAttribute('controls', ''); player.closest(selectors.videoParent).setAttribute('data-video-style', 'unmuted'); }); } }, stopMp4Video: function(id) { var player = this.container.querySelector('#' + id); if (player && typeof player.pause === 'function') { player.pause(); } }, /*============================================================================ Product images ==============================================================================*/ initImageZoom: function() { var container = this.container.querySelector(this.selectors.imageContainer); if (!container) { return; } var imageZoom = new theme.Photoswipe(container, this.sectionId); container.addEventListener('photoswipe:afterChange', function(evt) { if (this.flickity) { this.flickity.goToSlide(evt.detail.index); } }.bind(this)); }, getThumbIndex: function(target) { return target.dataset.index; }, updateVariantImage: function(evt) { var variant = evt.detail.variant; var sizedImgUrl = theme.Images.getSizedImageUrl(variant.featured_media.preview_image.src, this.settings.imageSize); var newImage = this.container.querySelector('.product__thumb[data-id="' + variant.featured_media.id + '"]'); var imageIndex = this.getThumbIndex(newImage); // If there is no index, slider is not initalized if (typeof imageIndex === 'undefined') { return; } // Go to that variant image's slide if (this.flickity) { this.flickity.goToSlide(imageIndex); } }, initProductSlider: function(variant) { // Stop if only a single image, but add active class to first slide if (this.cache.mainSlider.querySelectorAll(selectors.slide).length <= 1) { var slide = this.cache.mainSlider.querySelector(selectors.slide); if (slide) { slide.classList.add('is-selected'); } return; } // Destroy slider in preparation of new initialization if (this.flickity && typeof this.flickity.destroy === 'function') { this.flickity.destroy(); } // If variant argument exists, slideshow is reinitializing because of the // image set feature enabled and switching to a new group. // currentSlideIndex if (!variant) { var activeSlide = this.cache.mainSlider.querySelector(selectors.startingSlide); this.settings.currentSlideIndex = this._slideIndex(activeSlide); } var mainSliderArgs = { dragThreshold: 25, adaptiveHeight: true, avoidReflow: true, initialIndex: this.settings.currentSlideIndex, childNav: this.cache.thumbSlider, childNavScroller: this.cache.thumbScroller, childVertical: this.cache.thumbSlider.dataset.position === 'beside', pageDots: true, // mobile only with CSS wrapAround: true, callbacks: { onInit: this.onSliderInit.bind(this), onChange: this.onSlideChange.bind(this) } }; // Override default settings if image set feature enabled if (this.settings.imageSetName) { var imageSetArgs = this.imageSetArguments(variant); mainSliderArgs = Object.assign({}, mainSliderArgs, imageSetArgs); this.updateImageSetThumbs(mainSliderArgs.imageSet); } this.flickity = new theme.Slideshow(this.cache.mainSlider, mainSliderArgs); }, onSliderInit: function(slide) { // If slider is initialized with image set feature active, // initialize any videos/media when they are first slide if (this.settings.imageSetName) { this.prepMediaOnSlide(slide); } }, onSlideChange: function(index) { if (!this.flickity) return; var prevSlide = this.cache.mainSlider.querySelector('.product-main-slide[data-index="'+this.settings.currentSlideIndex+'"]'); // If imageSetName exists, use a more specific selector var nextSlide = this.settings.imageSetName ? this.cache.mainSlider.querySelectorAll('.flickity-slider .product-main-slide')[index] : this.cache.mainSlider.querySelector('.product-main-slide[data-index="'+index+'"]'); prevSlide.setAttribute('tabindex', '-1'); nextSlide.setAttribute('tabindex', 0); // Pause any existing slide video/media this.stopMediaOnSlide(prevSlide); // Prep next slide video/media this.prepMediaOnSlide(nextSlide); // Update current slider index this.settings.currentSlideIndex = index; }, stopMediaOnSlide(slide) { // Stop existing video var video = slide.querySelector(selectors.productVideo); if (video) { var videoType = this._getVideoType(video); var videoId = this._getVideoDivId(video); if (videoType === 'youtube') { if (videoObjects[videoId].videoPlayer) { videoObjects[videoId].videoPlayer.stopVideo(); return; } } else if (videoType === 'mp4') { this.stopMp4Video(videoId); return; } } // Stop existing media var currentMedia = slide.querySelector(this.selectors.media); if (currentMedia) { currentMedia.dispatchEvent( new CustomEvent('mediaHidden', { bubbles: true, cancelable: true }) ); } }, prepMediaOnSlide(slide) { var video = slide.querySelector(selectors.productVideo); if (video) { this.flickity.reposition(); var videoType = this._getVideoType(video); var videoId = this._getVideoDivId(video); if (videoType === 'youtube') { if (videoObjects[videoId].videoPlayer && videoObjects[videoId].options.style !== 'sound') { videoObjects[videoId].videoPlayer.playVideo(); return; } } else if (videoType === 'mp4') { this.playMp4Video(videoId); } } var nextMedia = slide.querySelector(this.selectors.media); if (nextMedia) { nextMedia.dispatchEvent( new CustomEvent('mediaVisible', { bubbles: true, cancelable: true }) ); slide.querySelector('.shopify-model-viewer-ui__button').setAttribute('tabindex', 0); slide.querySelector('.product-single__close-media').setAttribute('tabindex', 0); } }, _slideIndex: function(el) { return el.getAttribute('data-index'); }, /*============================================================================ Products when in quick view modal ==============================================================================*/ openModalProduct: function() { var initialized = false; if (!this.settings.modalInit) { this.blocksHolder = this.container.querySelector(selectors.blocksHolder); var url = this.blocksHolder.dataset.url; fetch(url).then(function(response) { return response.text(); }).then(function(html) { var parser = new DOMParser(); var doc = parser.parseFromString(html, 'text/html'); var blocks = doc.querySelector(selectors.blocks); // Because the same product could be opened in quick view // on the page we load the form elements from, we need to // update any `id`, `for`, and `form` attributes blocks.querySelectorAll('[id]').forEach(el => { // Update input `id` var val = el.getAttribute('id'); el.setAttribute('id', val + '-modal'); // Update related label if it exists var label = blocks.querySelector(`[for="${val}"]`); if (label) { label.setAttribute('for', val + '-modal'); } // Update any collapsible elements var collapsibleTrigger = blocks.querySelector(`[aria-controls="${val}"]`); if (collapsibleTrigger) { collapsibleTrigger.setAttribute('aria-controls', val + '-modal'); } }); // Update any elements with `form` attribute. // Form element already has `-modal` appended var form = blocks.querySelector(this.selectors.form); if (form) { var formId = form.getAttribute('id'); blocks.querySelectorAll('[form]').forEach(el => { el.setAttribute('form', formId); }); } this.blocksHolder.innerHTML = ''; this.blocksHolder.append(blocks); this.blocksHolder.classList.add('product-form-holder--loaded'); this.cacheElements(); this.formSetup(); this.updateModalProductInventory(); if (Shopify && Shopify.PaymentButton) { Shopify.PaymentButton.init(); } // Re-hook up collapsible box triggers theme.collapsibles.init(this.container); document.dispatchEvent(new CustomEvent('quickview:loaded', { detail: { productId: this.productId } })); }.bind(this)); this.productSetup(); this.videoSetup(); // Enable product slider in quick view // 1. with image sets enabled, make sure we have this.variants before initializing // 2. initialize normally, form data not required if (this.settings.imageSetName) { if (this.variants) { this.initProductSlider(); } else { document.addEventListener('quickview:loaded', function(evt) { if (evt.detail.productId === this.productId) { this.initProductSlider(); } }.bind(this)); } } else { this.initProductSlider(); } this.customMediaListners(); this.addIdToRecentlyViewed(); this.settings.modalInit = true; } else { initialized = true; } AOS.refreshHard(); document.dispatchEvent(new CustomEvent('quickview:open', { detail: { initialized: initialized, productId: this.productId } })); }, // Recommended products load via JS and don't add variant inventory to the // global variable that we later check. This function scrapes a data div // to get that info and manually add the values. updateModalProductInventory: function() { window.inventories = window.inventories || {}; this.container.querySelectorAll('.js-product-inventory-data').forEach(el => { var productId = el.dataset.productId; window.inventories[productId] = {}; el.querySelectorAll('.js-variant-inventory-data').forEach(el => { window.inventories[productId][el.dataset.id] = { 'quantity': el.dataset.quantity, 'policy': el.dataset.policy, 'incoming': el.dataset.incoming, 'next_incoming_date': el.dataset.date } }); }); }, closeModalProduct: function() { this.stopVideos(); }, /*============================================================================ Product media (3D) ==============================================================================*/ initModelViewerLibraries: function() { var modelViewerElements = this.container.querySelectorAll(this.selectors.media); if (modelViewerElements.length < 1) return; theme.ProductMedia.init(modelViewerElements, this.sectionId); }, initShopifyXrLaunch: function() { document.addEventListener( 'shopify_xr_launch', function() { var currentMedia = this.container.querySelector( this.selectors.productMediaWrapper + ':not(.' + self.classes.hidden + ')' ); currentMedia.dispatchEvent( new CustomEvent('xrLaunch', { bubbles: true, cancelable: true }) ); }.bind(this) ); }, customMediaListners: function() { document.querySelectorAll(this.selectors.closeMedia).forEach(el => { el.addEventListener('click', function() { var slide = this.cache.mainSlider.querySelector(selectors.currentSlide); var media = slide.querySelector(this.selectors.media); if (media) { media.dispatchEvent( new CustomEvent('mediaHidden', { bubbles: true, cancelable: true }) ); } }.bind(this)) }); var modelViewers = this.container.querySelectorAll('model-viewer'); if (modelViewers.length) { modelViewers.forEach(el => { el.addEventListener('shopify_model_viewer_ui_toggle_play', function(evt) { this.mediaLoaded(evt); }.bind(this)); el.addEventListener('shopify_model_viewer_ui_toggle_pause', function(evt) { this.mediaUnloaded(evt); }.bind(this)); }); } }, mediaLoaded: function(evt) { this.container.querySelectorAll(this.selectors.closeMedia).forEach(el => { el.classList.remove(classes.hidden); }); if (this.flickity) { this.flickity.setDraggable(false); } }, mediaUnloaded: function(evt) { this.container.querySelectorAll(this.selectors.closeMedia).forEach(el => { el.classList.add(classes.hidden); }); if (this.flickity) { this.flickity.setDraggable(true); } }, onUnload: function() { theme.ProductMedia.removeSectionModels(this.sectionId); if (this.flickity && typeof this.flickity.destroy === 'function') { this.flickity.destroy(); } } }); return Product; })(); theme.RecentlyViewed = (function() { var init = false; function RecentlyViewed(container) { if (!container) { return; } this.container = container; this.sectionId = this.container.getAttribute('data-section-id'); this.namespace = '.recently-viewed' + this.sectionId; this.gridItemWidth = this.container.getAttribute('data-grid-item-class'); this.rowOf = this.container.getAttribute('data-row-of'); this.imageSizes = this.container.getAttribute('data-image-sizes'); theme.initWhenVisible({ element: this.container, callback: this.init.bind(this), threshold: 600 }); }; RecentlyViewed.prototype = Object.assign({}, RecentlyViewed.prototype, { init: function() { if (init) { return; } init = true; if (Object.keys(theme.recentlyViewed.recent).length === 0 && theme.recentlyViewed.recent.constructor === Object) { // No previous history on page load, so bail this.container.classList.add('hide'); return; } this.outputContainer = document.getElementById('RecentlyViewed-' + this.sectionId); this.handle = this.container.getAttribute('data-product-handle'); // Request new product info via JS API var promises = []; Object.keys(theme.recentlyViewed.recent).forEach(function (handle) { if (handle !== 'undefined') { promises.push(this.getProductInfo(handle)); } }.bind(this)); Promise.all(promises).then(function(result) { this.setupOutput(result); this.captureProductDetails(result); }.bind(this)); }, getProductInfo: function(handle) { return new Promise(function(resolve, reject) { if (theme.recentlyViewed.productInfo.hasOwnProperty(handle)) { resolve(theme.recentlyViewed.productInfo[handle]); } else { fetch('/products/'+ handle +'.js').then(function(response) { return response.text(); }).then(function(product) { resolve(product); }); } }); }, setupOutput: function(products) { var allProducts = []; var data = {}; var limit = this.container.getAttribute('data-recent-count'); var i = 0; Object.keys(products).forEach(function (key) { if (!products[key]) { return; } var product = JSON.parse(products[key]); // Ignore current product if (product.handle === this.handle) { return; } // Ignore undefined key if (typeof product.handle == 'undefined') { return; } i++; // New or formatted properties const widths = [180, 360, 540, 720, 900]; product.url = theme.recentlyViewed.recent[product.handle] ? theme.recentlyViewed.recent[product.handle].url : product.url; product.image_responsive_url = theme.Images.buildImagePath(product.featured_image), product.image_responsive_urls = theme.Images.buildImagePath(product.featured_image, widths), product.image_aspect_ratio = theme.recentlyViewed.recent[product.handle].aspectRatio; // Unit pricing checks first variant var firstVariant = product.variants[0]; if (firstVariant && firstVariant.unit_price) { var baseUnit = ''; if (firstVariant.unit_price_measurement) { if (firstVariant.unit_price_measurement.reference_value != 1) { baseUnit += firstVariant.unit_price_measurement.reference_value + ' '; } baseUnit += firstVariant.unit_price_measurement.reference_unit; } product.unit_price = theme.Currency.formatMoney(firstVariant.unit_price); if (baseUnit != '') { product.unit_price += '/' + baseUnit; } } allProducts.unshift(product); }.bind(this)); if (allProducts.length === 0) { this.container.classList.add('hide'); return; } var productMarkup = theme.buildProductGridItem(allProducts.slice(0, limit), this.gridItemWidth, this.rowOf, this.imageSizes); this.outputContainer.innerHTML = productMarkup; if (AOS) { AOS.refreshHard(); } }, captureProductDetails: function(products) { for (var i = 0; i < products.length; i++) { var product = products[i]; theme.recentlyViewed.productInfo[product.handle] = product; } // Add data to session storage to reduce API requests later if (theme.config.hasSessionStorage) { sessionStorage.setItem('recent-products', JSON.stringify(theme.recentlyViewed.productInfo)); } }, onUnload: function() { init = false; } }); return RecentlyViewed; })(); theme.Testimonials = (function() { var defaults = { adaptiveHeight: true, avoidReflow: true, pageDots: true, prevNextButtons: false }; function Testimonials(container) { this.container = container; this.timeout; var sectionId = container.getAttribute('data-section-id'); this.slideshow = container.querySelector('#Testimonials-' + sectionId); this.namespace = '.testimonial-' + sectionId; if (!this.slideshow) { return } theme.initWhenVisible({ element: this.container, callback: this.init.bind(this), threshold: 600 }); } Testimonials.prototype = Object.assign({}, Testimonials.prototype, { init: function() { // Do not wrap when only a few blocks if (this.slideshow.dataset.count <= 3) { defaults.wrapAround = false; } this.flickity = new theme.Slideshow(this.slideshow, defaults); // Autoscroll to next slide on load to indicate more blocks if (this.slideshow.dataset.count > 2) { this.timeout = setTimeout(function() { this.flickity.goToSlide(1); }.bind(this), 1000); } }, onUnload: function() { if (this.flickity && typeof this.flickity.destroy === 'function') { this.flickity.destroy(); } }, onDeselect: function() { if (this.flickity && typeof this.flickity.play === 'function') { this.flickity.play(); } }, onBlockSelect: function(evt) { var slide = this.slideshow.querySelector('.testimonials-slide--' + evt.detail.blockId) var index = parseInt(slide.dataset.index); clearTimeout(this.timeout); if (this.flickity && typeof this.flickity.pause === 'function') { this.flickity.goToSlide(index); this.flickity.pause(); } }, onBlockDeselect: function() { if (this.flickity && typeof this.flickity.play === 'function') { this.flickity.play(); } } }); return Testimonials; })(); theme.isStorageSupported = function(type) { // Return false if we are in an iframe without access to sessionStorage if (window.self !== window.top) { return false; } var testKey = 'test'; var storage; if (type === 'session') { storage = window.sessionStorage; } if (type === 'local') { storage = window.localStorage; } try { storage.setItem(testKey, '1'); storage.removeItem(testKey); return true; } catch (error) { return false; } }; theme.reinitProductGridItem = function(scope) { if (AOS) {AOS.refreshHard()} theme.initQuickShop(); // Refresh reviews app if (window.SPR) {SPR.initDomEls();SPR.loadBadges()} // Re-hook up collapsible box triggers theme.collapsibles.init(); }; /*============================================================================ Things that don't require DOM to be ready ==============================================================================*/ theme.config.hasSessionStorage = theme.isStorageSupported('session'); theme.config.hasLocalStorage = theme.isStorageSupported('local'); AOS.init({ easing: 'ease-out-quad', once: true, offset: 60, disableMutationObserver: true }); if (theme.config.hasLocalStorage) { theme.recentlyViewed.localStorage = window.localStorage.getItem('theme-recent'); if (theme.recentlyViewed.localStorage) { theme.recentlyViewed.recent = JSON.parse(theme.recentlyViewed.localStorage); } } theme.recentlyViewed.productInfo = theme.config.hasSessionStorage && sessionStorage['recent-products'] ? JSON.parse(sessionStorage['recent-products']) : {}; // Trigger events when going between breakpoints theme.config.bpSmall = matchMedia(theme.config.mediaQuerySmall).matches; matchMedia(theme.config.mediaQuerySmall).addListener(function(mql) { if (mql.matches) { theme.config.bpSmall = true; document.dispatchEvent(new CustomEvent('matchSmall')); } else { theme.config.bpSmall = false; document.dispatchEvent(new CustomEvent('unmatchSmall')); } }); /*============================================================================ Things that require DOM to be ready ==============================================================================*/ function DOMready(callback) { if (document.readyState != 'loading') callback(); else document.addEventListener('DOMContentLoaded', callback); } // Load generic JS. Also reinitializes when sections are // added, edited, or removed in Shopify's editor theme.initGlobals = function() { theme.collapsibles.init(); theme.videoModal(); } DOMready(function(){ theme.sections = new theme.Sections(); theme.sections.register('slideshow-section', theme.SlideshowSection); theme.sections.register('header', theme.HeaderSection); theme.sections.register('product', theme.Product); theme.sections.register('blog', theme.Blog); theme.sections.register('password-header', theme.PasswordHeader); theme.sections.register('photoswipe', theme.Photoswipe); theme.sections.register('background-image', theme.BackgroundImage); theme.sections.register('testimonials', theme.Testimonials); theme.sections.register('video-section', theme.VideoSection); theme.sections.register('map', theme.Maps); theme.sections.register('footer-section', theme.FooterSection); theme.sections.register('store-availability', theme.StoreAvailability); theme.sections.register('recently-viewed', theme.RecentlyViewed); theme.sections.register('newsletter-popup', theme.NewsletterPopup); theme.sections.register('collection-header', theme.CollectionHeader); theme.sections.register('collection-grid', theme.Collection); theme.initGlobals(); theme.initQuickShop(); theme.rteInit(); if (document.body.classList.contains('template-cart')) { var cartPageForm = document.getElementById('CartPageForm'); if (cartPageForm) { new theme.CartForm(cartPageForm); } } if (theme.settings.predictiveSearch) { theme.predictiveSearch.init(); } if (theme.settings.isCustomerTemplate) { theme.customerTemplates(); } document.dispatchEvent(new CustomEvent('page:loaded')); }); })(); function add_to_cart_grid(variantid,qty){ var id = variantid; var q = qty; var ajax = { type: "POST", url: "/cart/add.js", data: "quantity=" + q + "&id=" + id, dataType: "json", success: function (n) { var cart = new theme.CartDrawer cart.init() cart.open(); }, error: function (n, c) { console.log('fail'); } }; jQuery.ajax(ajax) }