Skip to content
Closed

closed #3926

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions assets/standard-actions-override.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Standard Actions configuration for Dawn.
*
* Storefront Renderer injects the Shopify Standard Actions bundle
* (`window.Shopify.actions.{updateCart,openCart,getCart,…}`) into every
* page. The bundle ships with a built-in Dawn-aware refresh path that
* works out of the box.
*
* This file replaces the built-in path with an explicit, in-theme
* configuration. There are two reasons to do this:
*
* 1. Forks that change Dawn's cart contract — renaming custom
* elements, restructuring pubsub events, replacing
* getSectionsToRender, etc. — would silently break the built-in
* integration. Putting the wiring here keeps it visible and
* forkable.
*
* 2. The integration is now self-contained: anything you can read in
* this theme is what runs in the browser. There is no "magic"
* cart-refresh behavior happening elsewhere.
*
* If you remove this file, the built-in defaults take over.
*
* Configured actions:
* - openCart — opens Dawn's <cart-drawer>; falls back to /cart.
* - updateCart — after the Storefront API mutation, fetches affected
* sections, morphs the DOM, and publishes `cart-update` so Dawn's
* existing pubsub subscribers (cart-items, quick-add-bulk,
* recipient-form, price-per-item, …) react.
*
* Other actions (getCart, etc.) use the default implementation.
*/

// Cart custom elements that advertise sections via getSectionsToRender().
// If Dawn adds a new cart custom element, add its tag here.
const DAWN_CART_TAGS = ['cart-drawer', 'cart-items', 'cart-drawer-items', 'cart-notification'];

// Sections that Dawn's own pubsub subscribers refresh (cart.js's
// CartItems#onCartUpdate fetches and replaces these directly when
// cart-update fires; cart-drawer.js's renderContents handles the
// drawer body). We skip them here to avoid double-rendering.
// Format is '<element-tag>:<getSectionsToRender entry id>'.
// If you change which sections those subscribers refresh, update this set.
const DAWN_PUBSUB_REFRESHED_SECTIONS = new Set([
'cart-drawer:cart-drawer',
'cart-drawer-items:CartDrawer',
'cart-items:main-cart-items',
]);

// Walk every mounted Dawn cart custom element, collect the sections it
// wants rendered, and dedupe. Returns a Map keyed by section id, each
// entry pointing at the DOM mount and the selector used to extract the
// fresh fragment.
function collectCartSections() {
const sections = new Map();

for (const el of document.querySelectorAll(DAWN_CART_TAGS.join(','))) {
let entries;
try {
entries = el.getSectionsToRender?.();
} catch {
continue;
}

const tag = el.tagName.toLowerCase();
for (const entry of entries ?? []) {
if (DAWN_PUBSUB_REFRESHED_SECTIONS.has(`${tag}:${entry.id}`)) continue;

const sectionId = entry.section ?? entry.id;
if (!sectionId || sections.has(sectionId)) continue;

// Two patterns coexist in getSectionsToRender():
// - cart-items style: entry.section is the parent Liquid section
// id, entry.selector is a child node inside it.
// - cart-drawer / cart-notification style: entry.id IS the mount;
// there is no parent wrapper.
const root = entry.section ? document.getElementById(entry.id) : document;
if (!root) continue;

const mount = entry.selector
? (root.querySelector(entry.selector) ?? (entry.section ? root : null))
: document.getElementById(entry.id);
if (!mount) continue;

sections.set(sectionId, {
mount,
extractSelector: entry.selector || '.shopify-section',
});
}
}

return sections;
}

// After a Storefront API mutation, refresh every Dawn cart section
// that isn't already refreshed by Dawn's own pubsub subscribers, then
// publish 'cart-update' so the subscribers run.
//
// We always fetch /cart.js (with sections= when we have any) so that
// `cartData` is defined for subscribers. quick-add-bulk.js reads
// `event.cartData.items` unconditionally — publishing without cartData
// makes it throw.
async function refreshDawnCartUI() {
const sections = collectCartSections();
const sectionsQuery = sections.size
? `?sections=${[...sections.keys()].join(',')}`
: '';
const url = `${routes.cart_url}.js${sectionsQuery}`;
const cartData = await fetch(url, { headers: { Accept: 'application/json' } })
.then((r) => (r.ok ? r.json() : null))
.catch(() => null);

if (cartData?.sections) {
for (const [id, { mount, extractSelector }] of sections) {
const html = cartData.sections[id];
if (!html) continue;
const source = new DOMParser()
.parseFromString(html, 'text/html')
.querySelector(extractSelector);
if (source) mount.replaceChildren(...source.childNodes);
}
}

// Hand off to Dawn's existing subscribers. cartData is the full
// Cart Ajax payload (items, item_count, token, …) plus sections;
// quick-add-bulk.js and price-per-item.js read it directly.
publish(PUB_SUB_EVENTS.cartUpdate, {
source: 'external-refresh',
cartData: cartData ?? undefined,
});
}

function initStandardActions() {
const actions = window.Shopify?.actions;
if (!actions) return;

actions.openCart.configure({
async handler(defaultHandler) {
const drawer = document.querySelector('cart-drawer');
if (drawer && typeof drawer.open === 'function') {
drawer.open();
return;
}
return defaultHandler();
},
});

actions.updateCart.configure({
// Dawn doesn't currently listen for shopify:cart:* events, but the
// bundle requires an eventTarget. document is the conventional root.
eventTarget: () => document,
async handler(defaultHandler) {
const result = await defaultHandler();
try {
await refreshDawnCartUI();
} catch (error) {
console.error('[Dawn] Standard Actions cart refresh failed; reloading.', error);
window.location.reload();
}
return result;
},
});
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initStandardActions, { once: true });
} else {
initStandardActions();
}
1 change: 1 addition & 0 deletions layout/theme.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<script src="{{ 'details-disclosure.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'details-modal.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'search-form.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'standard-actions-override.js' | asset_url }}" defer="defer"></script>

{%- if settings.animations_reveal_on_scroll -%}
<script src="{{ 'animations.js' | asset_url }}" defer="defer"></script>
Expand All @@ -55,7 +56,7 @@
{{ settings.type_header_font | font_face: font_display: 'swap' }}

{% for scheme in settings.color_schemes -%}
{% assign scheme_classes = scheme_classes | append: ', .color-' | append: scheme.id %}

Check warning on line 59 in layout/theme.liquid

View workflow job for this annotation

GitHub Actions / Theme Check Report

layout/theme.liquid#L59

[UndefinedObject] Unknown object 'scheme_classes' used.
{% if forloop.index == 1 -%}
:root,
{%- endif %}
Expand Down
Loading