Skip to content

Instantly share code, notes, and snippets.

@sajanbasnet75
Created September 4, 2024 16:03
Show Gist options
  • Select an option

  • Save sajanbasnet75/80e95830a05a2b1291116775e03a1153 to your computer and use it in GitHub Desktop.

Select an option

Save sajanbasnet75/80e95830a05a2b1291116775e03a1153 to your computer and use it in GitHub Desktop.
import { FocusTrap, Delegate } from "vendor";
var DialogElement = class _DialogElement extends HTMLElement {
static get observedAttributes() {
return ["id", "open"];
}
static #lockLayerCount = 0;
#isLocked = false;
constructor() {
super();
this.addEventListener("dialog:force-close", (event) => {
this.hide();
event.stopPropagation();
});
}
connectedCallback() {
if (this.id) {
this.delegate.off().on("click", `[aria-controls="${this.id}"]`, this._onToggleClicked.bind(this));
}
this._abortController = new AbortController();
this.setAttribute("role", "dialog");
if (Shopify.designMode) {
this.addEventListener("shopify:block:select", (event) => this.show(!event.detail.load), { signal: this._abortController.signal });
this.addEventListener("shopify:block:deselect", this.hide, { signal: this._abortController.signal });
this._shopifySection = this._shopifySection || this.closest(".shopify-section");
if (this._shopifySection) {
if (this.hasAttribute("handle-section-events")) {
this._shopifySection.addEventListener("shopify:section:select", (event) => this.show(!event.detail.load), { signal: this._abortController.signal });
this._shopifySection.addEventListener("shopify:section:deselect", this.hide.bind(this), { signal: this._abortController.signal });
}
this._shopifySection.addEventListener("shopify:section:unload", () => this.remove(), { signal: this._abortController.signal });
}
}
}
disconnectedCallback() {
this._abortController.abort();
this.delegate.off();
this.focusTrap?.deactivate({ onDeactivate: () => {
} });
if (this.#isLocked) {
this.#isLocked = false;
document.documentElement.classList.toggle("lock", --_DialogElement.#lockLayerCount > 0);
}
}
show(animate19 = true) {
if (this.open) {
return;
}
this.setAttribute("open", animate19 ? "" : "immediate");
return waitForEvent(this, "dialog:after-show");
}
hide() {
if (!this.open) {
return;
}
this.removeAttribute("open");
return waitForEvent(this, "dialog:after-hide");
}
get delegate() {
return this._delegate = this._delegate || new Delegate(document.body);
}
get controls() {
return Array.from(this.getRootNode().querySelectorAll(`[aria-controls="${this.id}"]`));
}
get open() {
return this.hasAttribute("open");
}
get shouldTrapFocus() {
return true;
}
get shouldLock() {
return false;
}
/**
* Sometimes (especially for drawer) we need to ensure that an element is on top of everything else. To do that,
* we need to move the element to the body. We are doing that on open, and then restore the initial position on
* close
*/
get shouldAppendToBody() {
return false;
}
get initialFocus() {
return this.hasAttribute("initial-focus") ? this.getAttribute("initial-focus") : this.hasAttribute("tabindex") ? this : this.querySelector('input:not([type="hidden"])') || false;
}
get preventScrollWhenTrapped() {
return true;
}
get focusTrap() {
return this._focusTrap = this._focusTrap || new FocusTrap.createFocusTrap([this, this.shadowRoot], {
onDeactivate: this.hide.bind(this),
allowOutsideClick: this._allowOutsideClick.bind(this),
initialFocus: window.matchMedia("screen and (pointer: fine)").matches ? this.initialFocus : false,
fallbackFocus: this,
preventScroll: this.preventScrollWhenTrapped
});
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "id":
if (newValue) {
this.delegate.off().on("click", `[aria-controls="${this.id}"]`, this._onToggleClicked.bind(this));
}
break;
case "open":
this.controls.forEach((toggle) => toggle.setAttribute("aria-expanded", newValue === null ? "false" : "true"));
if (oldValue === null && (newValue === "" || newValue === "immediate")) {
this.removeAttribute("inert");
this._originalParentBeforeAppend = null;
if (this.shouldAppendToBody && this.parentElement !== document.body) {
this._originalParentBeforeAppend = this.parentElement;
document.body.append(this);
}
const showTransitionPromise = this._showTransition(newValue !== "immediate") || Promise.resolve();
showTransitionPromise.then(() => {
this.dispatchEvent(new CustomEvent("dialog:after-show", { bubbles: true }));
});
if (this.shouldTrapFocus) {
this.focusTrap.activate({
checkCanFocusTrap: () => showTransitionPromise
});
}
if (this.shouldLock) {
_DialogElement.#lockLayerCount += 1;
this.#isLocked = true;
document.documentElement.classList.add("lock");
}
} else if (oldValue !== null && newValue === null) {
this.setAttribute("inert", "");
const hideTransitionPromise = this._hideTransition() || Promise.resolve();
hideTransitionPromise.then(() => {
if (this.parentElement === document.body && this._originalParentBeforeAppend) {
this._originalParentBeforeAppend.appendChild(this);
this._originalParentBeforeAppend = null;
}
this.dispatchEvent(new CustomEvent("dialog:after-hide", { bubbles: true }));
});
this.focusTrap?.deactivate({
checkCanReturnFocus: () => hideTransitionPromise
});
if (this.shouldLock) {
this.#isLocked = false;
document.documentElement.classList.toggle("lock", --_DialogElement.#lockLayerCount > 0);
}
}
this.dispatchEvent(new CustomEvent("toggle", { bubbles: true }));
break;
}
}
/* Those methods are used to perform custom show/hide transition, and must return a promise */
_showTransition(animate19 = true) {
}
_hideTransition() {
}
/* By default, a focus element is deactivated when you click outside it */
_allowOutsideClick(event) {
if ("TouchEvent" in window && event instanceof TouchEvent) {
return this._allowOutsideClickTouch(event);
} else {
return this._allowOutsideClickMouse(event);
}
}
_allowOutsideClickTouch(event) {
event.target.addEventListener("touchend", (subEvent) => {
const endTarget = document.elementFromPoint(subEvent.changedTouches.item(0).clientX, subEvent.changedTouches.item(0).clientY);
if (!this.contains(endTarget)) {
this.hide();
}
}, { once: true });
return false;
}
_allowOutsideClickMouse(event) {
if (event.type !== "click") {
return false;
}
if (!this.contains(event.target)) {
this.hide();
}
let target = event.target, closestControl = event.target.closest("[aria-controls]");
if (closestControl && closestControl.getAttribute("aria-controls") === this.id) {
target = closestControl;
}
return this.id !== target.getAttribute("aria-controls");
}
_onToggleClicked(event) {
event?.preventDefault();
this.open ? this.hide() : this.show();
}
};
var CloseButton = class extends HTMLButtonElement {
constructor() {
super();
this.addEventListener("click", () => this.dispatchEvent(new CustomEvent("dialog:force-close", { bubbles: true, cancelable: true, composed: true })));
}
};
if (!window.customElements.get("dialog-element")) {
window.customElements.define("dialog-element", DialogElement);
}
if (!window.customElements.get("close-button")) {
window.customElements.define("close-button", CloseButton, { extends: "button" });
}
import { animate as motionAnimate, timeline as motionTimeline } from "vendor";
var reduceDrawerAnimation = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.themeVariables.settings.reduceDrawerAnimation;
var Drawer = class extends DialogElement {
constructor() {
super();
const template2 = document.getElementById(this.template).content.cloneNode(true);
this.attachShadow({ mode: "open" }).appendChild(template2);
this.shadowRoot.addEventListener("slotchange", (event) => this._updateSlotVisibility(event.target));
}
connectedCallback() {
super.connectedCallback();
this.setAttribute("aria-modal", "true");
this.shadowRoot.querySelector('[part="overlay"]')?.addEventListener("click", this.hide.bind(this), { signal: this._abortController.signal });
Array.from(this.shadowRoot.querySelectorAll("slot")).forEach((slot) => this._updateSlotVisibility(slot));
}
get template() {
return this.getAttribute("template") || "drawer-default-template";
}
get shouldLock() {
return true;
}
get shouldAppendToBody() {
return true;
}
get openFrom() {
return window.matchMedia(`${window.themeVariables.breakpoints["sm-max"]}`).matches ? "bottom" : this.getAttribute("open-from") || "right";
}
_getClipPathProperties() {
switch (this.openFrom) {
case "left":
return document.dir === "ltr" ? ["inset(0 100% 0 0 round var(--rounded-sm))", "inset(0 0 0 0 round var(--rounded-sm))"] : ["inset(0 0 0 100% round var(--rounded-sm))", "inset(0 0 0 0 round var(--rounded-sm))"];
case "right":
return document.dir === "ltr" ? ["inset(0 0 0 100% round var(--rounded-sm))", "inset(0 0 0 0 round var(--rounded-sm)"] : ["inset(0 100% 0 0 round var(--rounded-sm))", "inset(0 0 0 0 round var(--rounded-sm))"];
case "bottom":
return ["inset(100% 0 0 0 round var(--rounded-sm))", "inset(0 0 0 0 round var(--rounded-sm))"];
case "top":
return ["inset(0 0 100% 0 round var(--rounded-sm))", "inset(0 0 0 0 round var(--rounded-sm))"];
}
}
_setInitialPosition() {
this.style.left = document.dir === "ltr" && this.openFrom === "left" || document.dir === "rtl" && this.openFrom === "right" ? "0px" : "auto";
this.style.right = this.style.left === "auto" ? "0px" : "auto";
this.style.bottom = this.openFrom === "bottom" ? "0px" : null;
this.style.top = this.style.bottom === "" ? "0px" : null;
}
_showTransition(animate19 = true) {
let animationControls;
this._setInitialPosition();
if (reduceDrawerAnimation) {
animationControls = motionAnimate(this, { opacity: [0, 1], visibility: ["hidden", "visible"] }, { duration: 0.2 });
} else {
let content = this.shadowRoot.querySelector('[part="content"]'), closeButton = this.shadowRoot.querySelector('[part="outside-close-button"]');
animationControls = motionTimeline([
[this, { opacity: [0, 1], visibility: ["hidden", "visible"] }, { duration: 0.15 }],
[content, { clipPath: this._getClipPathProperties() }, { duration: 0.4, easing: [0.86, 0, 0.07, 1] }],
[content.children, { opacity: [0, 1] }, { duration: 0.15 }],
[closeButton, { opacity: [0, 1] }, { at: "<", duration: 0.15 }]
]);
}
animate19 ? animationControls.play() : animationControls.finish();
return animationControls.finished.then(() => this.classList.add("show-close-cursor"));
}
_hideTransition() {
let animationControls;
if (reduceDrawerAnimation) {
animationControls = motionAnimate(this, { opacity: [1, 0], visibility: ["visibility", "hidden"] }, { duration: 0.2 });
} else {
let content = this.shadowRoot.querySelector('[part="content"]'), closeButton = this.shadowRoot.querySelector('[part="outside-close-button"]');
animationControls = motionTimeline([
[closeButton, { opacity: [null, 0] }, { duration: 0.15 }],
[content.children, { opacity: [null, 0] }, { at: "<", duration: 0.15 }],
[content, { clipPath: this._getClipPathProperties().reverse() }, { duration: 0.4, easing: [0.86, 0, 0.07, 1] }],
[this, { opacity: [null, 0], visibility: ["visible", "hidden"] }, { duration: 0.15 }]
]);
}
return animationControls.finished.then(() => this.classList.remove("show-close-cursor"));
}
_updateSlotVisibility(slot) {
if (!["header", "footer", "body"].includes(slot.name)) {
return;
}
slot.parentElement.hidden = slot.assignedElements({ flatten: true }).length === 0;
}
};
var CartDrawer = class extends Drawer {
constructor() {
super();
this._onPrepareBundledSectionsListener = this._onPrepareBundledSections.bind(this);
this._onCartChangedListener = this._onCartChanged.bind(this);
this._onCartRefreshListener = this._onCartRefresh.bind(this);
this._onVariantAddedListener = this._onVariantAdded.bind(this);
window.addEventListener("pageshow", this._onPageShow.bind(this));
}
connectedCallback() {
super.connectedCallback();
document.addEventListener("cart:prepare-bundled-sections", this._onPrepareBundledSectionsListener);
document.addEventListener("cart:change", this._onCartChangedListener);
document.addEventListener("cart:refresh", this._onCartRefreshListener);
document.addEventListener("variant:add", this._onVariantAddedListener);
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("cart:prepare-bundled-sections", this._onPrepareBundledSectionsListener);
document.removeEventListener("cart:change", this._onCartChangedListener);
document.removeEventListener("cart:refresh", this._onCartRefreshListener);
document.removeEventListener("variant:add", this._onVariantAddedListener);
}
get shouldAppendToBody() {
return false;
}
get openFrom() {
return "right";
}
_onPrepareBundledSections(event) {
event.detail.sections.push(extractSectionId(this));
}
/**
* Update the cart drawer content when cart content changes.
*/
async _onCartChanged(event) {
const updatedDrawerContent = new DOMParser().parseFromString(event.detail.cart["sections"][extractSectionId(this)], "text/html");
if (event.detail.cart["item_count"] > 0) {
const currentInner = this.querySelector(".cart-drawer__inner"), updatedInner = updatedDrawerContent.querySelector(".cart-drawer__inner");
if (!currentInner) {
this.replaceChildren(document.createRange().createContextualFragment(updatedDrawerContent.querySelector(".cart-drawer").innerHTML));
} else {
setTimeout(() => {
currentInner.innerHTML = updatedInner.innerHTML;
// Fetch product recommendations and update it.
const cartDrawerRecommendations = new CartDrawerRecommendations();
cartDrawerRecommendations.displayCartRecommendations()
}, event.detail.baseEvent === "variant:add" ? 0 : 1250);
this.querySelector('[slot="footer"]').replaceChildren(...updatedDrawerContent.querySelector('[slot="footer"]').childNodes);
}
} else {
await animate4(this.children, { opacity: 0 }, { duration: 0.15 }).finished;
this.replaceChildren(...updatedDrawerContent.querySelector(".cart-drawer").childNodes);
animate4(this.querySelector(".empty-state"), { opacity: [0, 1], transform: ["translateY(20px)", "translateY(0)"] }, { duration: 0.15 });
}
}
/**
* Handle the case when the page is served from BF cache
*/
_onPageShow(event) {
if (!event.persisted) {
return;
}
this._onCartRefresh();
}
/**
* Listeners called when a new variant has been added
*/
_onVariantAdded(event) {
if (window.themeVariables.settings.cartType !== "drawer" || event.detail?.blockCartDrawerOpening) {
return;
}
this.show();
}
/**
* Force a complete refresh of the cart drawer (this is called by dispatching the 'cart:refresh' on the document)
*/
async _onCartRefresh() {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = await (await fetch(`${window.Shopify.routes.root}?section_id=${extractSectionId(this)}`)).text();
this.replaceChildren(...tempDiv.querySelector("#cart-drawer").children);
}
};
// the cart icon in in the header navigation
<a href="{{ routes.cart_url }}" data-no-instant class="relative tap-area" {% if settings.cart_type != 'page' and request.page_type != 'cart' %}aria-controls="cart-drawer"{% endif %}>
<span class="sr-only">{{ 'header.general.open_cart' | t }}</span>
{%- render 'icon' with 'cart' -%}
<div class="header__cart-count">
<cart-count class="count-bubble {% if cart.item_count == 0 %}opacity-0{% endif %}" aria-hidden="true">
{{- cart.item_count -}}
</cart-count>
</div>
</a>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment