@altazion/commerce-sdk-htmx 26.407.7565 → 26.415.7673
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -5
- package/dist/index.cjs +965 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +82 -7
- package/dist/index.iife.js +966 -142
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +965 -140
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,697 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const commerceSdkCore = require("@altazion/commerce-sdk-core");
|
|
4
|
+
const EXTENSION_NAME = "altazion";
|
|
5
|
+
const JSON_VALUES_ATTRIBUTE = "hx-vals";
|
|
6
|
+
const REFRESH_ATTRIBUTE = "hx-altazion-refresh";
|
|
7
|
+
const REFRESH_EVENT_NAME = "altazion:refresh";
|
|
8
|
+
const REQUEST_ATTRIBUTES = [
|
|
9
|
+
{ attribute: "hx-get", method: "GET" },
|
|
10
|
+
{ attribute: "hx-post", method: "POST" },
|
|
11
|
+
{ attribute: "hx-put", method: "PUT" },
|
|
12
|
+
{ attribute: "hx-patch", method: "PATCH" },
|
|
13
|
+
{ attribute: "hx-delete", method: "DELETE" }
|
|
14
|
+
];
|
|
15
|
+
function createAltazionExtension(client, modules) {
|
|
16
|
+
const requestHandler = (event) => {
|
|
9
17
|
const e = event;
|
|
10
18
|
e.detail.headers["Accept-Language"] = client.context.locale;
|
|
11
19
|
e.detail.headers["X-Altazion-Currency"] = client.context.currency;
|
|
12
20
|
};
|
|
13
|
-
window
|
|
21
|
+
if (typeof window !== "undefined" && window.htmx) {
|
|
22
|
+
window.htmx.on("htmx:configRequest", requestHandler);
|
|
23
|
+
}
|
|
24
|
+
if (typeof document === "undefined") {
|
|
25
|
+
return {
|
|
26
|
+
dispose() {
|
|
27
|
+
if (typeof window !== "undefined" && window.htmx) {
|
|
28
|
+
window.htmx.off("htmx:configRequest", requestHandler);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const handleClick = (event) => {
|
|
34
|
+
const mouseEvent = event;
|
|
35
|
+
if (mouseEvent.defaultPrevented || mouseEvent.button !== 0 || mouseEvent.metaKey || mouseEvent.ctrlKey || mouseEvent.shiftKey || mouseEvent.altKey) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const target = event.target;
|
|
39
|
+
if (!(target instanceof Element)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const matchedAction = findMatchedAction(target, modules);
|
|
43
|
+
if (!matchedAction || matchedAction.element instanceof HTMLFormElement || !hasAltazionExtension(matchedAction.element)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
void runModuleAction(client, matchedAction);
|
|
48
|
+
};
|
|
49
|
+
const handleSubmit = (event) => {
|
|
50
|
+
const target = event.target;
|
|
51
|
+
if (!(target instanceof HTMLFormElement)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const matchedAction = findMatchedAction(target, modules);
|
|
55
|
+
if (!matchedAction || matchedAction.element !== target || !hasAltazionExtension(target)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
void runModuleAction(client, matchedAction);
|
|
60
|
+
};
|
|
61
|
+
document.addEventListener("click", handleClick);
|
|
62
|
+
document.addEventListener("submit", handleSubmit);
|
|
63
|
+
return {
|
|
64
|
+
dispose() {
|
|
65
|
+
if (typeof window !== "undefined" && window.htmx) {
|
|
66
|
+
window.htmx.off("htmx:configRequest", requestHandler);
|
|
67
|
+
}
|
|
68
|
+
document.removeEventListener("click", handleClick);
|
|
69
|
+
document.removeEventListener("submit", handleSubmit);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function requireString(value, fieldName) {
|
|
74
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
75
|
+
return value.trim();
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Missing payload field: ${fieldName}`);
|
|
78
|
+
}
|
|
79
|
+
function requireNumber(value, fieldName) {
|
|
80
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
84
|
+
const parsed = Number(value);
|
|
85
|
+
if (Number.isFinite(parsed)) {
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Invalid payload field: ${fieldName}`);
|
|
90
|
+
}
|
|
91
|
+
function asRecord(value) {
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
async function runModuleAction(client, matchedAction) {
|
|
98
|
+
var _a;
|
|
99
|
+
const { definition, element } = matchedAction;
|
|
100
|
+
const action = readActionName(element, definition);
|
|
101
|
+
const payload = collectPayload(element, definition);
|
|
102
|
+
const releasePending = markPending(element);
|
|
103
|
+
dispatchModuleEvent(element, buildModuleEventName(definition.moduleName, "before"), {
|
|
104
|
+
module: definition.moduleName,
|
|
105
|
+
action,
|
|
106
|
+
element,
|
|
107
|
+
payload
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
const result = await definition.execute(client, action, payload);
|
|
111
|
+
dispatchModuleEvent(element, buildModuleEventName(definition.moduleName, "after"), {
|
|
112
|
+
module: definition.moduleName,
|
|
113
|
+
action,
|
|
114
|
+
element,
|
|
115
|
+
payload,
|
|
116
|
+
result
|
|
117
|
+
});
|
|
118
|
+
const successEventKind = resolveSuccessEventKind(definition, action, result, payload);
|
|
119
|
+
const emittedSuffixes = /* @__PURE__ */ new Set();
|
|
120
|
+
if (successEventKind !== "none") {
|
|
121
|
+
emittedSuffixes.add(successEventKind);
|
|
122
|
+
dispatchModuleEvent(element, buildModuleEventName(definition.moduleName, successEventKind), {
|
|
123
|
+
module: definition.moduleName,
|
|
124
|
+
action,
|
|
125
|
+
element,
|
|
126
|
+
payload,
|
|
127
|
+
result
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (const suffix of ((_a = definition.getResultEventSuffixes) == null ? void 0 : _a.call(definition, action, result, payload)) ?? []) {
|
|
131
|
+
if (emittedSuffixes.has(suffix)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
dispatchModuleEvent(element, buildModuleEventName(definition.moduleName, suffix), {
|
|
135
|
+
module: definition.moduleName,
|
|
136
|
+
action,
|
|
137
|
+
element,
|
|
138
|
+
payload,
|
|
139
|
+
result
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
await refreshTargets(element);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const errorDetail = normalizeError(error);
|
|
145
|
+
dispatchModuleEvent(element, buildModuleEventName(definition.moduleName, "error"), {
|
|
146
|
+
module: definition.moduleName,
|
|
147
|
+
action,
|
|
148
|
+
element,
|
|
149
|
+
payload,
|
|
150
|
+
error,
|
|
151
|
+
errorDetail
|
|
152
|
+
});
|
|
153
|
+
console.error(`[AltazionHtmx] ${definition.moduleName} action failed`, error);
|
|
154
|
+
} finally {
|
|
155
|
+
releasePending();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function findMatchedAction(target, modules) {
|
|
159
|
+
let current = target;
|
|
160
|
+
while (current) {
|
|
161
|
+
for (const definition of modules) {
|
|
162
|
+
if (current.hasAttribute(definition.actionAttribute)) {
|
|
163
|
+
return { definition, element: current };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
current = current.parentElement;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function readActionName(element, definition) {
|
|
171
|
+
const action = element.getAttribute(definition.actionAttribute);
|
|
172
|
+
if (!action) {
|
|
173
|
+
throw new Error(`Missing ${definition.actionAttribute} attribute`);
|
|
174
|
+
}
|
|
175
|
+
return definition.parseAction(action);
|
|
176
|
+
}
|
|
177
|
+
function collectPayload(element, definition) {
|
|
178
|
+
var _a;
|
|
179
|
+
const payload = {};
|
|
180
|
+
const form = element instanceof HTMLFormElement ? element : element.closest("form");
|
|
181
|
+
if (form) {
|
|
182
|
+
Object.assign(payload, formDataToObject(new FormData(form)));
|
|
183
|
+
mergeJsonValues(payload, form);
|
|
184
|
+
applyAttributePayload(payload, form, definition.payloadAttributes);
|
|
185
|
+
applyJsonAttributePayload(payload, form, definition.jsonPayloadAttributes);
|
|
186
|
+
}
|
|
187
|
+
mergeJsonValues(payload, element);
|
|
188
|
+
applyAttributePayload(payload, element, definition.payloadAttributes);
|
|
189
|
+
applyJsonAttributePayload(payload, element, definition.jsonPayloadAttributes);
|
|
190
|
+
applyPayloadAliases(payload, definition.payloadAliases);
|
|
191
|
+
(_a = definition.normalizePayload) == null ? void 0 : _a.call(definition, payload);
|
|
192
|
+
return payload;
|
|
193
|
+
}
|
|
194
|
+
function mergeJsonValues(payload, element) {
|
|
195
|
+
const values = readJsonValues(element, JSON_VALUES_ATTRIBUTE);
|
|
196
|
+
if (values) {
|
|
197
|
+
Object.assign(payload, values);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function applyAttributePayload(payload, element, attributes) {
|
|
201
|
+
if (!attributes) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
for (const [attributeName, payloadKey] of Object.entries(attributes)) {
|
|
205
|
+
const value = element.getAttribute(attributeName);
|
|
206
|
+
if (value !== null) {
|
|
207
|
+
payload[payloadKey] = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function applyJsonAttributePayload(payload, element, attributes) {
|
|
212
|
+
if (!attributes) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
for (const [attributeName, payloadKey] of Object.entries(attributes)) {
|
|
216
|
+
const value = readJsonValues(element, attributeName);
|
|
217
|
+
if (value !== void 0) {
|
|
218
|
+
payload[payloadKey] = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function applyPayloadAliases(payload, aliases) {
|
|
223
|
+
if (!aliases) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const [alias, canonicalKey] of Object.entries(aliases)) {
|
|
227
|
+
if (payload[alias] !== void 0 && payload[canonicalKey] === void 0) {
|
|
228
|
+
payload[canonicalKey] = payload[alias];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function formDataToObject(formData) {
|
|
233
|
+
const payload = {};
|
|
234
|
+
for (const [key, value] of formData.entries()) {
|
|
235
|
+
const normalizedValue = typeof value === "string" ? value : value.name;
|
|
236
|
+
const previous = payload[key];
|
|
237
|
+
if (previous === void 0) {
|
|
238
|
+
payload[key] = normalizedValue;
|
|
239
|
+
} else if (Array.isArray(previous)) {
|
|
240
|
+
previous.push(normalizedValue);
|
|
241
|
+
} else {
|
|
242
|
+
payload[key] = [previous, normalizedValue];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return payload;
|
|
246
|
+
}
|
|
247
|
+
function dispatchModuleEvent(element, eventName, detail) {
|
|
248
|
+
element.dispatchEvent(new CustomEvent(eventName, {
|
|
249
|
+
bubbles: true,
|
|
250
|
+
detail
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
function buildModuleEventName(moduleName, suffix) {
|
|
254
|
+
return `altazion:${moduleName}:${suffix}`;
|
|
255
|
+
}
|
|
256
|
+
function resolveSuccessEventKind(definition, action, result, payload) {
|
|
257
|
+
const configuredKind = definition.successEventKind;
|
|
258
|
+
if (typeof configuredKind === "function") {
|
|
259
|
+
return configuredKind(action, result, payload);
|
|
260
|
+
}
|
|
261
|
+
if (configuredKind) {
|
|
262
|
+
return configuredKind;
|
|
263
|
+
}
|
|
264
|
+
return inferSuccessEventKind(action, result);
|
|
265
|
+
}
|
|
266
|
+
function inferSuccessEventKind(action, result) {
|
|
267
|
+
if (result === void 0) {
|
|
268
|
+
return "changed";
|
|
269
|
+
}
|
|
270
|
+
const normalizedAction = action.toLowerCase();
|
|
271
|
+
if (normalizedAction.startsWith("get") || normalizedAction.startsWith("find") || normalizedAction.startsWith("search") || normalizedAction.startsWith("suggest") || normalizedAction.startsWith("load") || normalizedAction.startsWith("list")) {
|
|
272
|
+
return "loaded";
|
|
273
|
+
}
|
|
274
|
+
return "updated";
|
|
275
|
+
}
|
|
276
|
+
function normalizeError(error) {
|
|
277
|
+
if (error instanceof commerceSdkCore.AltazionApiError) {
|
|
278
|
+
return {
|
|
279
|
+
name: error.name,
|
|
280
|
+
message: error.message,
|
|
281
|
+
status: error.status,
|
|
282
|
+
problem: error.problem,
|
|
283
|
+
isOffline: false,
|
|
284
|
+
isConflict: error.status === 409
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (error instanceof commerceSdkCore.OfflineError) {
|
|
288
|
+
return {
|
|
289
|
+
name: error.name,
|
|
290
|
+
message: error.message,
|
|
291
|
+
isOffline: true,
|
|
292
|
+
isConflict: false
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (error instanceof Error) {
|
|
296
|
+
return {
|
|
297
|
+
name: error.name,
|
|
298
|
+
message: error.message,
|
|
299
|
+
isOffline: false,
|
|
300
|
+
isConflict: false
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
name: "UnknownError",
|
|
305
|
+
message: "Unknown error",
|
|
306
|
+
isOffline: false,
|
|
307
|
+
isConflict: false
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async function refreshTargets(element) {
|
|
311
|
+
const targets = resolveRefreshTargets(element);
|
|
312
|
+
if (targets.length === 0) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
await Promise.all(targets.map((target) => refreshTargetElement(element, target)));
|
|
316
|
+
}
|
|
317
|
+
function resolveRefreshTargets(element) {
|
|
318
|
+
if (typeof document === "undefined") {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
const selectors = collectRefreshSelectors(element);
|
|
322
|
+
if (selectors.length === 0) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
const targets = /* @__PURE__ */ new Set();
|
|
326
|
+
for (const selector of selectors) {
|
|
327
|
+
try {
|
|
328
|
+
for (const target of document.querySelectorAll(selector)) {
|
|
329
|
+
targets.add(target);
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(`[AltazionHtmx] invalid refresh selector: ${selector}`, error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return [...targets];
|
|
336
|
+
}
|
|
337
|
+
function collectRefreshSelectors(element) {
|
|
338
|
+
const values = /* @__PURE__ */ new Set();
|
|
339
|
+
const form = element instanceof HTMLFormElement ? element : element.closest("form");
|
|
340
|
+
addRefreshSelectors(values, form);
|
|
341
|
+
addRefreshSelectors(values, element);
|
|
342
|
+
return [...values];
|
|
343
|
+
}
|
|
344
|
+
function addRefreshSelectors(values, element) {
|
|
345
|
+
const rawValue = element == null ? void 0 : element.getAttribute(REFRESH_ATTRIBUTE);
|
|
346
|
+
if (!rawValue) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
for (const selector of rawValue.split(";")) {
|
|
350
|
+
const trimmed = selector.trim();
|
|
351
|
+
if (trimmed.length > 0) {
|
|
352
|
+
values.add(trimmed);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function refreshTargetElement(sourceElement, targetElement) {
|
|
357
|
+
var _a;
|
|
358
|
+
const request = readRefreshRequest(targetElement);
|
|
359
|
+
if (request && typeof window !== "undefined" && ((_a = window.htmx) == null ? void 0 : _a.ajax)) {
|
|
360
|
+
const context = {
|
|
361
|
+
source: sourceElement,
|
|
362
|
+
target: targetElement
|
|
363
|
+
};
|
|
364
|
+
const swap = targetElement.getAttribute("hx-swap");
|
|
365
|
+
if (swap) {
|
|
366
|
+
context.swap = swap;
|
|
367
|
+
}
|
|
368
|
+
const select = targetElement.getAttribute("hx-select");
|
|
369
|
+
if (select) {
|
|
370
|
+
context.select = select;
|
|
371
|
+
}
|
|
372
|
+
const values = readJsonValues(targetElement, JSON_VALUES_ATTRIBUTE);
|
|
373
|
+
if (values) {
|
|
374
|
+
context.values = values;
|
|
375
|
+
}
|
|
376
|
+
await Promise.resolve(window.htmx.ajax(request.method, request.path, context));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
targetElement.dispatchEvent(new CustomEvent(REFRESH_EVENT_NAME, {
|
|
380
|
+
bubbles: true,
|
|
381
|
+
detail: {
|
|
382
|
+
source: sourceElement,
|
|
383
|
+
target: targetElement
|
|
384
|
+
}
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
function readRefreshRequest(element) {
|
|
388
|
+
for (const requestAttribute of REQUEST_ATTRIBUTES) {
|
|
389
|
+
const path = element.getAttribute(requestAttribute.attribute);
|
|
390
|
+
if (path) {
|
|
391
|
+
return {
|
|
392
|
+
method: requestAttribute.method,
|
|
393
|
+
path
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function readJsonValues(element, attributeName) {
|
|
400
|
+
const raw = element.getAttribute(attributeName);
|
|
401
|
+
if (!raw) {
|
|
402
|
+
return void 0;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const parsed = JSON.parse(raw);
|
|
406
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
407
|
+
return parsed;
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
throw new Error(`Invalid JSON in ${attributeName}`);
|
|
411
|
+
}
|
|
412
|
+
return void 0;
|
|
413
|
+
}
|
|
414
|
+
function markPending(element) {
|
|
415
|
+
const previousBusy = element.getAttribute("aria-busy");
|
|
416
|
+
element.setAttribute("aria-busy", "true");
|
|
417
|
+
const disableable = isDisableableElement(element) ? element : null;
|
|
418
|
+
const previousDisabled = (disableable == null ? void 0 : disableable.disabled) ?? false;
|
|
419
|
+
if (disableable) {
|
|
420
|
+
disableable.disabled = true;
|
|
421
|
+
}
|
|
422
|
+
return () => {
|
|
423
|
+
if (previousBusy === null) {
|
|
424
|
+
element.removeAttribute("aria-busy");
|
|
425
|
+
} else {
|
|
426
|
+
element.setAttribute("aria-busy", previousBusy);
|
|
427
|
+
}
|
|
428
|
+
if (disableable) {
|
|
429
|
+
disableable.disabled = previousDisabled;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function isDisableableElement(element) {
|
|
434
|
+
return "disabled" in element;
|
|
435
|
+
}
|
|
436
|
+
function hasAltazionExtension(element) {
|
|
437
|
+
let current = element;
|
|
438
|
+
while (current) {
|
|
439
|
+
const extensions = current.getAttribute("hx-ext");
|
|
440
|
+
if (extensions && parseExtensions(extensions).includes(EXTENSION_NAME)) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
current = current.parentElement;
|
|
444
|
+
}
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
function parseExtensions(value) {
|
|
448
|
+
return value.split(/[\s,]+/).map((extension) => extension.trim()).filter((extension) => extension.length > 0);
|
|
449
|
+
}
|
|
450
|
+
const cartModuleDefinition = {
|
|
451
|
+
moduleName: "cart",
|
|
452
|
+
actionAttribute: "hx-altazion-cart-action",
|
|
453
|
+
payloadAttributes: {
|
|
454
|
+
"hx-altazion-cart-reference": "reference",
|
|
455
|
+
"hx-altazion-cart-quantity": "quantity",
|
|
456
|
+
"hx-altazion-cart-line-id": "lineId",
|
|
457
|
+
"hx-altazion-cart-code": "code"
|
|
458
|
+
},
|
|
459
|
+
jsonPayloadAttributes: {
|
|
460
|
+
"hx-altazion-cart-options": "options"
|
|
461
|
+
},
|
|
462
|
+
payloadAliases: {
|
|
463
|
+
ref: "reference",
|
|
464
|
+
productReference: "reference",
|
|
465
|
+
qty: "quantity",
|
|
466
|
+
line: "lineId",
|
|
467
|
+
coupon: "code"
|
|
468
|
+
},
|
|
469
|
+
parseAction(actionValue) {
|
|
470
|
+
switch (actionValue) {
|
|
471
|
+
case "getCart":
|
|
472
|
+
case "getValidationStatus":
|
|
473
|
+
case "addItem":
|
|
474
|
+
case "updateItem":
|
|
475
|
+
case "removeItem":
|
|
476
|
+
case "applyCoupon":
|
|
477
|
+
case "removeCoupon":
|
|
478
|
+
return actionValue;
|
|
479
|
+
default:
|
|
480
|
+
throw new Error(`Unsupported cart action: ${actionValue}`);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
execute(client, action, payload) {
|
|
484
|
+
switch (action) {
|
|
485
|
+
case "getCart":
|
|
486
|
+
return client.cart.getCart();
|
|
487
|
+
case "getValidationStatus":
|
|
488
|
+
return client.cart.getValidationStatus();
|
|
489
|
+
case "addItem": {
|
|
490
|
+
const reference = requireString(payload.reference, "reference");
|
|
491
|
+
const quantity = requireNumber(payload.quantity, "quantity");
|
|
492
|
+
const options = buildAddItemOptions(payload);
|
|
493
|
+
return client.cart.addItem(reference, quantity, options);
|
|
494
|
+
}
|
|
495
|
+
case "updateItem": {
|
|
496
|
+
const lineId = requireString(payload.lineId, "lineId");
|
|
497
|
+
const quantity = requireNumber(payload.quantity, "quantity");
|
|
498
|
+
return client.cart.updateItem(lineId, quantity);
|
|
499
|
+
}
|
|
500
|
+
case "removeItem":
|
|
501
|
+
return client.cart.removeItem(requireString(payload.lineId, "lineId"));
|
|
502
|
+
case "applyCoupon":
|
|
503
|
+
return client.cart.applyCoupon(requireString(payload.code, "code"));
|
|
504
|
+
case "removeCoupon":
|
|
505
|
+
return client.cart.removeCoupon(requireString(payload.code, "code"));
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
successEventKind(action) {
|
|
509
|
+
switch (action) {
|
|
510
|
+
case "getCart":
|
|
511
|
+
return "loaded";
|
|
512
|
+
case "getValidationStatus":
|
|
513
|
+
return "none";
|
|
514
|
+
case "addItem":
|
|
515
|
+
case "updateItem":
|
|
516
|
+
case "removeItem":
|
|
517
|
+
case "applyCoupon":
|
|
518
|
+
case "removeCoupon":
|
|
519
|
+
return "updated";
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
getResultEventSuffixes(_action, result) {
|
|
523
|
+
return isCart(result) ? ["updated"] : ["status"];
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
function buildAddItemOptions(payload) {
|
|
527
|
+
const options = asRecord(payload.options);
|
|
528
|
+
const extraEntries = Object.entries(payload).filter(([key]) => ![
|
|
529
|
+
"reference",
|
|
530
|
+
"ref",
|
|
531
|
+
"productReference",
|
|
532
|
+
"quantity",
|
|
533
|
+
"qty",
|
|
534
|
+
"options"
|
|
535
|
+
].includes(key));
|
|
536
|
+
if (extraEntries.length === 0 && !options) {
|
|
537
|
+
return void 0;
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
...Object.fromEntries(extraEntries),
|
|
541
|
+
...options
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function isCart(result) {
|
|
545
|
+
return "guid" in result;
|
|
14
546
|
}
|
|
547
|
+
const sessionModuleDefinition = {
|
|
548
|
+
moduleName: "session",
|
|
549
|
+
actionAttribute: "hx-altazion-session-action",
|
|
550
|
+
parseAction(actionValue) {
|
|
551
|
+
switch (actionValue) {
|
|
552
|
+
case "getSession":
|
|
553
|
+
return actionValue;
|
|
554
|
+
default:
|
|
555
|
+
throw new Error(`Unsupported session action: ${actionValue}`);
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
execute(client, action) {
|
|
559
|
+
switch (action) {
|
|
560
|
+
case "getSession":
|
|
561
|
+
return client.session.getSession();
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
successEventKind() {
|
|
565
|
+
return "loaded";
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
const marketingModuleDefinition = {
|
|
569
|
+
moduleName: "marketing",
|
|
570
|
+
actionAttribute: "hx-altazion-marketing-action",
|
|
571
|
+
payloadAttributes: {
|
|
572
|
+
"hx-altazion-marketing-code": "code"
|
|
573
|
+
},
|
|
574
|
+
jsonPayloadAttributes: {
|
|
575
|
+
"hx-altazion-marketing-codes": "codes"
|
|
576
|
+
},
|
|
577
|
+
parseAction(actionValue) {
|
|
578
|
+
switch (actionValue) {
|
|
579
|
+
case "getItem":
|
|
580
|
+
case "getItems":
|
|
581
|
+
return actionValue;
|
|
582
|
+
default:
|
|
583
|
+
throw new Error(`Unsupported marketing action: ${actionValue}`);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
execute(client, action, payload) {
|
|
587
|
+
switch (action) {
|
|
588
|
+
case "getItem":
|
|
589
|
+
return client.marketing.getItem(requireString(payload.code, "code"));
|
|
590
|
+
case "getItems":
|
|
591
|
+
return client.marketing.getItems(readCodes(payload));
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
successEventKind() {
|
|
595
|
+
return "loaded";
|
|
596
|
+
},
|
|
597
|
+
getResultEventSuffixes(action) {
|
|
598
|
+
switch (action) {
|
|
599
|
+
case "getItem":
|
|
600
|
+
return ["item-loaded"];
|
|
601
|
+
case "getItems":
|
|
602
|
+
return ["items-loaded"];
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
function readCodes(payload) {
|
|
607
|
+
const value = payload.codes;
|
|
608
|
+
if (Array.isArray(value)) {
|
|
609
|
+
const codes = value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
610
|
+
if (codes.length > 0) {
|
|
611
|
+
return codes;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (typeof value === "string") {
|
|
615
|
+
const codes = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
616
|
+
if (codes.length > 0) {
|
|
617
|
+
return codes;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (typeof payload.code === "string" && payload.code.trim().length > 0) {
|
|
621
|
+
return [payload.code.trim()];
|
|
622
|
+
}
|
|
623
|
+
throw new Error("Missing payload field: codes");
|
|
624
|
+
}
|
|
625
|
+
const storesModuleDefinition = {
|
|
626
|
+
moduleName: "stores",
|
|
627
|
+
actionAttribute: "hx-altazion-stores-action",
|
|
628
|
+
payloadAttributes: {
|
|
629
|
+
"hx-altazion-stores-store-guid": "storeGuid",
|
|
630
|
+
"hx-altazion-stores-latitude": "latitude",
|
|
631
|
+
"hx-altazion-stores-longitude": "longitude",
|
|
632
|
+
"hx-altazion-stores-radius-km": "radiusKm",
|
|
633
|
+
"hx-altazion-stores-postal-code": "postalCode",
|
|
634
|
+
"hx-altazion-stores-country-code": "countryCode"
|
|
635
|
+
},
|
|
636
|
+
payloadAliases: {
|
|
637
|
+
lat: "latitude",
|
|
638
|
+
lon: "longitude",
|
|
639
|
+
lng: "longitude",
|
|
640
|
+
radius: "radiusKm",
|
|
641
|
+
storeId: "storeGuid",
|
|
642
|
+
id: "storeGuid",
|
|
643
|
+
postal: "postalCode",
|
|
644
|
+
country: "countryCode"
|
|
645
|
+
},
|
|
646
|
+
parseAction(actionValue) {
|
|
647
|
+
switch (actionValue) {
|
|
648
|
+
case "findByLocation":
|
|
649
|
+
case "findByPostalCode":
|
|
650
|
+
case "getStore":
|
|
651
|
+
return actionValue;
|
|
652
|
+
default:
|
|
653
|
+
throw new Error(`Unsupported stores action: ${actionValue}`);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
execute(client, action, payload) {
|
|
657
|
+
switch (action) {
|
|
658
|
+
case "findByLocation": {
|
|
659
|
+
const latitude = requireNumber(payload.latitude, "latitude");
|
|
660
|
+
const longitude = requireNumber(payload.longitude, "longitude");
|
|
661
|
+
const radiusKm = payload.radiusKm === void 0 ? 50 : requireNumber(payload.radiusKm, "radiusKm");
|
|
662
|
+
return client.stores.findByLocation(latitude, longitude, radiusKm);
|
|
663
|
+
}
|
|
664
|
+
case "findByPostalCode": {
|
|
665
|
+
const postalCode = requireString(payload.postalCode, "postalCode");
|
|
666
|
+
const countryCode = payload.countryCode === void 0 ? "FR" : requireString(payload.countryCode, "countryCode");
|
|
667
|
+
return client.stores.findByPostalCode(postalCode, countryCode);
|
|
668
|
+
}
|
|
669
|
+
case "getStore":
|
|
670
|
+
return client.stores.getStore(requireString(payload.storeGuid, "storeGuid"));
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
successEventKind() {
|
|
674
|
+
return "loaded";
|
|
675
|
+
},
|
|
676
|
+
getResultEventSuffixes(action) {
|
|
677
|
+
switch (action) {
|
|
678
|
+
case "getStore":
|
|
679
|
+
return ["store-loaded"];
|
|
680
|
+
case "findByLocation":
|
|
681
|
+
case "findByPostalCode":
|
|
682
|
+
return ["stores-loaded"];
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
function registerAltazionExtension(client) {
|
|
687
|
+
return createAltazionExtension(client, [
|
|
688
|
+
cartModuleDefinition,
|
|
689
|
+
sessionModuleDefinition,
|
|
690
|
+
marketingModuleDefinition,
|
|
691
|
+
storesModuleDefinition
|
|
692
|
+
]);
|
|
693
|
+
}
|
|
694
|
+
const registerAltazionAuthExtension = registerAltazionExtension;
|
|
15
695
|
function registerPriceHelpers(hbs, defaults) {
|
|
16
696
|
hbs.registerHelper(
|
|
17
697
|
"formatPrice",
|
|
@@ -141,133 +821,153 @@ function registerCartHelpers(hbs, getCart, defaults) {
|
|
|
141
821
|
}, 0);
|
|
142
822
|
});
|
|
143
823
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
824
|
+
const DATA_SOURCE_ATTRIBUTE = "hx-altazion-data-source";
|
|
825
|
+
const DATA_STATUS_ATTRIBUTE = "hx-altazion-data-status";
|
|
826
|
+
const RENDER_TEMPLATE_ATTRIBUTE = "hx-render-template";
|
|
827
|
+
const DATA_SOURCE_ALIASES = {
|
|
828
|
+
cart: "cart",
|
|
829
|
+
session: "session",
|
|
830
|
+
store: "store",
|
|
831
|
+
stores: "stores",
|
|
832
|
+
"marketing-item": "marketingItem",
|
|
833
|
+
"marketing-items": "marketingItems"
|
|
834
|
+
};
|
|
835
|
+
function createAltazionDeclarativeRenderer(handlebars) {
|
|
836
|
+
const state = {
|
|
837
|
+
cart: null,
|
|
838
|
+
session: null,
|
|
839
|
+
store: null,
|
|
840
|
+
stores: [],
|
|
841
|
+
marketingItem: null,
|
|
842
|
+
marketingItems: [],
|
|
843
|
+
slots: {}
|
|
844
|
+
};
|
|
845
|
+
const templateCache = /* @__PURE__ */ new Map();
|
|
846
|
+
const render = () => {
|
|
847
|
+
if (typeof document === "undefined") {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
for (const element of document.querySelectorAll(`[${DATA_SOURCE_ATTRIBUTE}]`)) {
|
|
851
|
+
const rawSource = element.getAttribute(DATA_SOURCE_ATTRIBUTE);
|
|
852
|
+
const source = resolveDataSource(rawSource);
|
|
853
|
+
if (!source) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const rawStatus = element.getAttribute(DATA_STATUS_ATTRIBUTE);
|
|
857
|
+
const sourceValue = resolveSourceValue(state, source);
|
|
858
|
+
const isVisible = matchesStatus(rawStatus, sourceValue);
|
|
859
|
+
applyVisibility(element, isVisible);
|
|
860
|
+
renderElementTemplate(element, isVisible, state, sourceValue, handlebars, templateCache);
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
return {
|
|
864
|
+
update(key, value) {
|
|
865
|
+
state[key] = value;
|
|
866
|
+
render();
|
|
867
|
+
},
|
|
868
|
+
updateSlot(name, value) {
|
|
869
|
+
state.slots[name] = value;
|
|
870
|
+
render();
|
|
871
|
+
},
|
|
872
|
+
render,
|
|
873
|
+
dispose() {
|
|
874
|
+
templateCache.clear();
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function matchesStatus(status, value) {
|
|
879
|
+
if (Array.isArray(value)) {
|
|
880
|
+
switch (status) {
|
|
881
|
+
case "empty":
|
|
882
|
+
return value.length === 0;
|
|
883
|
+
case "not-empty":
|
|
884
|
+
return value.length > 0;
|
|
885
|
+
default:
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
switch (status) {
|
|
890
|
+
case "null":
|
|
891
|
+
return value == null;
|
|
892
|
+
case "not-null":
|
|
893
|
+
return value != null;
|
|
894
|
+
default:
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function resolveDataSource(source) {
|
|
899
|
+
if (!source) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
if (source.startsWith("slot:")) {
|
|
903
|
+
const slotKey = source.slice(5).trim();
|
|
904
|
+
return slotKey.length > 0 ? { kind: "slot", key: slotKey } : null;
|
|
905
|
+
}
|
|
906
|
+
const resolvedKey = DATA_SOURCE_ALIASES[source];
|
|
907
|
+
return resolvedKey ? { kind: "state", key: resolvedKey } : null;
|
|
908
|
+
}
|
|
909
|
+
function resolveSourceValue(state, source) {
|
|
910
|
+
if (source.kind === "slot") {
|
|
911
|
+
return state.slots[source.key] ?? null;
|
|
912
|
+
}
|
|
913
|
+
return state[source.key];
|
|
914
|
+
}
|
|
915
|
+
function applyVisibility(element, isVisible) {
|
|
916
|
+
element.setAttribute("aria-hidden", isVisible ? "false" : "true");
|
|
917
|
+
if (element instanceof HTMLElement) {
|
|
918
|
+
element.hidden = !isVisible;
|
|
919
|
+
} else if (isVisible) {
|
|
920
|
+
element.removeAttribute("hidden");
|
|
921
|
+
} else {
|
|
922
|
+
element.setAttribute("hidden", "");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function renderElementTemplate(element, isVisible, state, sourceValue, handlebars, templateCache) {
|
|
926
|
+
if (!isVisible || !element.hasAttribute(RENDER_TEMPLATE_ATTRIBUTE) || !handlebars) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const templateId = element.getAttribute(RENDER_TEMPLATE_ATTRIBUTE);
|
|
930
|
+
if (!templateId) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const template = resolveTemplate(templateId, handlebars, templateCache);
|
|
934
|
+
if (!template) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
element.innerHTML = template({
|
|
938
|
+
...state,
|
|
939
|
+
slot: sourceValue
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
function resolveTemplate(templateId, handlebars, templateCache) {
|
|
943
|
+
const cached = templateCache.get(templateId);
|
|
944
|
+
if (cached) {
|
|
945
|
+
return cached;
|
|
946
|
+
}
|
|
947
|
+
if (typeof document === "undefined") {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
const templateElement = document.getElementById(templateId);
|
|
951
|
+
if (!templateElement) {
|
|
952
|
+
console.warn(`[AltazionHtmx] Template not found: ${templateId}`);
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
const compiled = handlebars.compile(templateElement.innerHTML);
|
|
956
|
+
templateCache.set(templateId, compiled);
|
|
957
|
+
return compiled;
|
|
958
|
+
}
|
|
959
|
+
const productCardTemplate = '<article class="altz-product-card{{#unless (isAvailable availability)}} altz-product-card--unavailable{{/unless}}">\n {{#if imageUrl}}\n <a href="{{productUrl reference}}" class="altz-product-card__image-link" aria-label="{{name}}">\n <img\n src="{{thumbnailUrl imageUrl 400}}"\n alt="{{name}}"\n class="altz-product-card__image"\n loading="lazy"\n width="400"\n />\n </a>\n {{/if}}\n\n <div class="altz-product-card__body">\n <a href="{{productUrl reference}}" class="altz-product-card__title">\n {{name}}\n </a>\n\n <div class="altz-product-card__pricing">\n {{#if discount}}\n <span class="altz-product-card__price altz-product-card__price--discounted">\n {{formatPrice price}}\n </span>\n <span class="altz-product-card__price altz-product-card__price--original">\n {{formatPrice originalPrice}}\n </span>\n <span class="altz-product-card__discount-badge">\n {{discountPercent originalPrice price}}\n </span>\n {{else}}\n <span class="altz-product-card__price">\n {{formatPrice price}}\n </span>\n {{/if}}\n </div>\n\n {{#unless (isAvailable availability)}}\n <p class="altz-product-card__unavailable">{{unavailableLabel}}</p>\n {{/unless}}\n\n {{#if (isAvailable availability)}}\n <button\n class="altz-product-card__add-to-cart"\n hx-ext="altazion"\n hx-altazion-cart-action="addItem"\n hx-altazion-cart-reference="{{reference}}"\n hx-altazion-cart-quantity="1"\n hx-altazion-refresh="#altz-cart-mini"\n hx-indicator=".altz-spinner"\n >\n {{addToCartLabel}}\n </button>\n {{/if}}\n </div>\n</article>\n';
|
|
199
960
|
const cartMiniTemplate = '<div id="altz-cart-mini" class="altz-cart-mini">\n <div class="altz-cart-mini__header">\n <span class="altz-cart-mini__label">{{cartLabel}}</span>\n <span class="altz-cart-mini__count">{{cartCount}}</span>\n </div>\n\n {{#cartHasItems}}\n <ul class="altz-cart-mini__lines">\n {{#each lines}}\n <li class="altz-cart-mini__line">\n <span class="altz-cart-mini__line-name">{{this.productName}}</span>\n <span class="altz-cart-mini__line-qty">× {{this.quantity}}</span>\n <span class="altz-cart-mini__line-price">{{formatPrice this.unitPriceWithTax}}</span>\n </li>\n {{/each}}\n </ul>\n\n <div class="altz-cart-mini__total">\n <span>{{totalLabel}}</span>\n <strong>{{cartTotal}}</strong>\n </div>\n\n <a href="{{cartUrl}}" class="altz-cart-mini__cta">\n {{viewCartLabel}}\n </a>\n {{/cartHasItems}}\n\n {{^cartHasItems}}\n <p class="altz-cart-mini__empty">{{emptyLabel}}</p>\n {{/cartHasItems}}\n</div>\n';
|
|
200
|
-
const productListTemplate =
|
|
201
|
-
|
|
202
|
-
<h2 class="altz-product-list__title">{{title}}</h2>
|
|
203
|
-
{{/if}}
|
|
204
|
-
|
|
205
|
-
{{#if products.length}}
|
|
206
|
-
<ul class="altz-product-list__grid" role="list">
|
|
207
|
-
{{#each products}}
|
|
208
|
-
<li class="altz-product-list__item">
|
|
209
|
-
<article class="altz-product-card{{#unless (isAvailable this.availability)}} altz-product-card--unavailable{{/unless}}">
|
|
210
|
-
{{#if this.imageUrl}}
|
|
211
|
-
<a href="{{productUrl this.reference}}" class="altz-product-card__image-link" aria-label="{{this.name}}">
|
|
212
|
-
<img
|
|
213
|
-
src="{{thumbnailUrl this.imageUrl 300}}"
|
|
214
|
-
alt="{{this.name}}"
|
|
215
|
-
class="altz-product-card__image"
|
|
216
|
-
loading="lazy"
|
|
217
|
-
width="300"
|
|
218
|
-
/>
|
|
219
|
-
</a>
|
|
220
|
-
{{/if}}
|
|
221
|
-
|
|
222
|
-
<div class="altz-product-card__body">
|
|
223
|
-
<a href="{{productUrl this.reference}}" class="altz-product-card__title">
|
|
224
|
-
{{this.name}}
|
|
225
|
-
</a>
|
|
226
|
-
|
|
227
|
-
<div class="altz-product-card__pricing">
|
|
228
|
-
{{#if this.discount}}
|
|
229
|
-
<span class="altz-product-card__price altz-product-card__price--discounted">
|
|
230
|
-
{{formatPrice this.price}}
|
|
231
|
-
</span>
|
|
232
|
-
<span class="altz-product-card__price altz-product-card__price--original">
|
|
233
|
-
{{formatPrice this.originalPrice}}
|
|
234
|
-
</span>
|
|
235
|
-
<span class="altz-product-card__discount-badge">
|
|
236
|
-
{{discountPercent this.originalPrice this.price}}
|
|
237
|
-
</span>
|
|
238
|
-
{{else}}
|
|
239
|
-
<span class="altz-product-card__price">
|
|
240
|
-
{{formatPrice this.price}}
|
|
241
|
-
</span>
|
|
242
|
-
{{/if}}
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
{{#if (isAvailable this.availability)}}
|
|
246
|
-
<button
|
|
247
|
-
class="altz-product-card__add-to-cart"
|
|
248
|
-
hx-post="/commerce/api/process/cart/lines"
|
|
249
|
-
hx-vals='{"productReference":"{{this.reference}}","quantity":1}'
|
|
250
|
-
hx-target="#altz-cart-summary"
|
|
251
|
-
hx-swap="outerHTML"
|
|
252
|
-
hx-indicator=".altz-spinner"
|
|
253
|
-
>
|
|
254
|
-
{{../addToCartLabel}}
|
|
255
|
-
</button>
|
|
256
|
-
{{/if}}
|
|
257
|
-
</div>
|
|
258
|
-
</article>
|
|
259
|
-
</li>
|
|
260
|
-
{{/each}}
|
|
261
|
-
</ul>
|
|
262
|
-
{{else}}
|
|
263
|
-
<p class="altz-product-list__empty">{{emptyLabel}}</p>
|
|
264
|
-
{{/if}}
|
|
265
|
-
</section>
|
|
266
|
-
`;
|
|
267
|
-
function init(config) {
|
|
961
|
+
const productListTemplate = '<section class="altz-product-list">\n {{#if title}}\n <h2 class="altz-product-list__title">{{title}}</h2>\n {{/if}}\n\n {{#if products.length}}\n <ul class="altz-product-list__grid" role="list">\n {{#each products}}\n <li class="altz-product-list__item">\n <article class="altz-product-card{{#unless (isAvailable this.availability)}} altz-product-card--unavailable{{/unless}}">\n {{#if this.imageUrl}}\n <a href="{{productUrl this.reference}}" class="altz-product-card__image-link" aria-label="{{this.name}}">\n <img\n src="{{thumbnailUrl this.imageUrl 300}}"\n alt="{{this.name}}"\n class="altz-product-card__image"\n loading="lazy"\n width="300"\n />\n </a>\n {{/if}}\n\n <div class="altz-product-card__body">\n <a href="{{productUrl this.reference}}" class="altz-product-card__title">\n {{this.name}}\n </a>\n\n <div class="altz-product-card__pricing">\n {{#if this.discount}}\n <span class="altz-product-card__price altz-product-card__price--discounted">\n {{formatPrice this.price}}\n </span>\n <span class="altz-product-card__price altz-product-card__price--original">\n {{formatPrice this.originalPrice}}\n </span>\n <span class="altz-product-card__discount-badge">\n {{discountPercent this.originalPrice this.price}}\n </span>\n {{else}}\n <span class="altz-product-card__price">\n {{formatPrice this.price}}\n </span>\n {{/if}}\n </div>\n\n {{#if (isAvailable this.availability)}}\n <button\n class="altz-product-card__add-to-cart"\n hx-ext="altazion"\n hx-altazion-cart-action="addItem"\n hx-altazion-cart-reference="{{this.reference}}"\n hx-altazion-cart-quantity="1"\n hx-altazion-refresh="#altz-cart-mini"\n hx-indicator=".altz-spinner"\n >\n {{../addToCartLabel}}\n </button>\n {{/if}}\n </div>\n </article>\n </li>\n {{/each}}\n </ul>\n {{else}}\n <p class="altz-product-list__empty">{{emptyLabel}}</p>\n {{/if}}\n</section>\n';
|
|
962
|
+
function initAltazionHtmx(config) {
|
|
268
963
|
var _a;
|
|
269
|
-
const {
|
|
270
|
-
|
|
964
|
+
const {
|
|
965
|
+
client,
|
|
966
|
+
handlebars,
|
|
967
|
+
offlineSelector = "body",
|
|
968
|
+
terminalMode
|
|
969
|
+
} = config;
|
|
970
|
+
const extensionHandle = registerAltazionExtension(client);
|
|
271
971
|
if (typeof window !== "undefined" && window.htmx) {
|
|
272
972
|
window.htmx.config = window.htmx.config ?? {};
|
|
273
973
|
window.htmx.config.withCredentials = true;
|
|
@@ -276,24 +976,91 @@ function init(config) {
|
|
|
276
976
|
locale: client.context.locale,
|
|
277
977
|
currency: client.context.currency
|
|
278
978
|
};
|
|
979
|
+
const handlebarsRuntime = handlebars ?? (typeof window !== "undefined" ? window.Handlebars : void 0);
|
|
980
|
+
const declarativeRenderer = createAltazionDeclarativeRenderer(handlebarsRuntime);
|
|
981
|
+
const syncSlotFromEvent = (detail) => {
|
|
982
|
+
var _a2;
|
|
983
|
+
const slotName = (_a2 = detail.element.getAttribute("hx-altazion-data-slot")) == null ? void 0 : _a2.trim();
|
|
984
|
+
if (!slotName) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
declarativeRenderer.updateSlot(slotName, detail.result ?? null);
|
|
988
|
+
};
|
|
279
989
|
let currentCart = null;
|
|
280
990
|
const getCart = () => currentCart;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
991
|
+
const syncCartFromEvent = (event) => {
|
|
992
|
+
const customEvent = event;
|
|
993
|
+
if (customEvent.detail.result) {
|
|
994
|
+
currentCart = customEvent.detail.result;
|
|
995
|
+
declarativeRenderer.update("cart", currentCart);
|
|
287
996
|
}
|
|
997
|
+
syncSlotFromEvent(customEvent.detail);
|
|
998
|
+
};
|
|
999
|
+
const syncSessionFromEvent = (event) => {
|
|
1000
|
+
const customEvent = event;
|
|
1001
|
+
declarativeRenderer.update("session", customEvent.detail.result ?? null);
|
|
1002
|
+
syncSlotFromEvent(customEvent.detail);
|
|
1003
|
+
};
|
|
1004
|
+
const syncStoreFromEvent = (event) => {
|
|
1005
|
+
const customEvent = event;
|
|
1006
|
+
declarativeRenderer.update("store", customEvent.detail.result ?? null);
|
|
1007
|
+
syncSlotFromEvent(customEvent.detail);
|
|
1008
|
+
};
|
|
1009
|
+
const syncStoresFromEvent = (event) => {
|
|
1010
|
+
const customEvent = event;
|
|
1011
|
+
declarativeRenderer.update("stores", Array.isArray(customEvent.detail.result) ? customEvent.detail.result : []);
|
|
1012
|
+
syncSlotFromEvent(customEvent.detail);
|
|
1013
|
+
};
|
|
1014
|
+
const syncMarketingItemFromEvent = (event) => {
|
|
1015
|
+
const customEvent = event;
|
|
1016
|
+
declarativeRenderer.update("marketingItem", Array.isArray(customEvent.detail.result) ? null : customEvent.detail.result ?? null);
|
|
1017
|
+
syncSlotFromEvent(customEvent.detail);
|
|
1018
|
+
};
|
|
1019
|
+
const syncMarketingItemsFromEvent = (event) => {
|
|
1020
|
+
const customEvent = event;
|
|
1021
|
+
declarativeRenderer.update("marketingItems", Array.isArray(customEvent.detail.result) ? customEvent.detail.result : []);
|
|
1022
|
+
syncSlotFromEvent(customEvent.detail);
|
|
1023
|
+
};
|
|
1024
|
+
if (typeof document !== "undefined") {
|
|
1025
|
+
document.addEventListener("altazion:cart:loaded", syncCartFromEvent);
|
|
1026
|
+
document.addEventListener("altazion:cart:updated", syncCartFromEvent);
|
|
1027
|
+
document.addEventListener("altazion:session:loaded", syncSessionFromEvent);
|
|
1028
|
+
document.addEventListener("altazion:stores:store-loaded", syncStoreFromEvent);
|
|
1029
|
+
document.addEventListener("altazion:stores:stores-loaded", syncStoresFromEvent);
|
|
1030
|
+
document.addEventListener("altazion:marketing:item-loaded", syncMarketingItemFromEvent);
|
|
1031
|
+
document.addEventListener("altazion:marketing:items-loaded", syncMarketingItemsFromEvent);
|
|
1032
|
+
}
|
|
1033
|
+
void client.cart.getCart().then((cart) => {
|
|
1034
|
+
currentCart = cart;
|
|
1035
|
+
declarativeRenderer.update("cart", cart);
|
|
1036
|
+
}).catch(() => {
|
|
288
1037
|
});
|
|
289
1038
|
if (handlebars) {
|
|
290
1039
|
registerPriceHelpers(handlebars, defaults);
|
|
291
1040
|
registerProductHelpers(handlebars, client.context.siteUrl ?? "");
|
|
292
1041
|
registerCartHelpers(handlebars, getCart, defaults);
|
|
293
1042
|
}
|
|
294
|
-
|
|
1043
|
+
declarativeRenderer.render();
|
|
1044
|
+
const offlineEl = typeof document !== "undefined" ? document.querySelector(offlineSelector) ?? document.body : null;
|
|
1045
|
+
let lastConnectivityStatus = null;
|
|
295
1046
|
const updateOfflineClass = () => {
|
|
296
|
-
|
|
1047
|
+
if (!offlineEl) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const status = client.isOffline ? "offline" : "online";
|
|
1051
|
+
const isOffline = status === "offline";
|
|
1052
|
+
offlineEl.classList.toggle("altz-offline", isOffline);
|
|
1053
|
+
offlineEl.classList.toggle("altz-terminal-offline", isOffline);
|
|
1054
|
+
offlineEl.classList.toggle("altz-terminal-online", !isOffline);
|
|
1055
|
+
offlineEl.setAttribute("data-altazion-terminal-state", status);
|
|
1056
|
+
updateManagedVisibility(resolveManagedElements(terminalMode == null ? void 0 : terminalMode.interactiveSelectors), isOffline, "altz-terminal-interactive-hidden");
|
|
1057
|
+
updateManagedVisibility(resolveManagedElements(terminalMode == null ? void 0 : terminalMode.offlineScreenSelectors), !isOffline, "altz-terminal-offline-screen-hidden");
|
|
1058
|
+
if (lastConnectivityStatus === status) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
lastConnectivityStatus = status;
|
|
1062
|
+
dispatchConnectivityEvent(offlineEl, "altazion:connectivity:changed", status);
|
|
1063
|
+
dispatchConnectivityEvent(offlineEl, isOffline ? "altazion:offline" : "altazion:online", status);
|
|
297
1064
|
};
|
|
298
1065
|
updateOfflineClass();
|
|
299
1066
|
const unsubscribeConnectivity = (_a = client.connectivity) == null ? void 0 : _a.subscribe(() => updateOfflineClass());
|
|
@@ -310,12 +1077,70 @@ function init(config) {
|
|
|
310
1077
|
return {
|
|
311
1078
|
templates,
|
|
312
1079
|
dispose() {
|
|
1080
|
+
extensionHandle.dispose();
|
|
1081
|
+
if (typeof document !== "undefined") {
|
|
1082
|
+
document.removeEventListener("altazion:cart:loaded", syncCartFromEvent);
|
|
1083
|
+
document.removeEventListener("altazion:cart:updated", syncCartFromEvent);
|
|
1084
|
+
document.removeEventListener("altazion:session:loaded", syncSessionFromEvent);
|
|
1085
|
+
document.removeEventListener("altazion:stores:store-loaded", syncStoreFromEvent);
|
|
1086
|
+
document.removeEventListener("altazion:stores:stores-loaded", syncStoresFromEvent);
|
|
1087
|
+
document.removeEventListener("altazion:marketing:item-loaded", syncMarketingItemFromEvent);
|
|
1088
|
+
document.removeEventListener("altazion:marketing:items-loaded", syncMarketingItemsFromEvent);
|
|
1089
|
+
}
|
|
1090
|
+
declarativeRenderer.dispose();
|
|
313
1091
|
unsubscribeConnectivity == null ? void 0 : unsubscribeConnectivity();
|
|
314
1092
|
}
|
|
315
1093
|
};
|
|
316
1094
|
}
|
|
1095
|
+
const init = initAltazionHtmx;
|
|
1096
|
+
function resolveManagedElements(selectorInput) {
|
|
1097
|
+
if (typeof document === "undefined" || !selectorInput) {
|
|
1098
|
+
return [];
|
|
1099
|
+
}
|
|
1100
|
+
const selectors = Array.isArray(selectorInput) ? selectorInput : [selectorInput];
|
|
1101
|
+
const elements = /* @__PURE__ */ new Set();
|
|
1102
|
+
for (const selector of selectors) {
|
|
1103
|
+
if (!selector.trim()) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
for (const element of document.querySelectorAll(selector)) {
|
|
1108
|
+
elements.add(element);
|
|
1109
|
+
}
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return [...elements];
|
|
1114
|
+
}
|
|
1115
|
+
function updateManagedVisibility(elements, hidden, hiddenClassName) {
|
|
1116
|
+
for (const element of elements) {
|
|
1117
|
+
element.classList.toggle(hiddenClassName, hidden);
|
|
1118
|
+
element.setAttribute("aria-hidden", hidden ? "true" : "false");
|
|
1119
|
+
if (element instanceof HTMLElement) {
|
|
1120
|
+
element.hidden = hidden;
|
|
1121
|
+
} else if (hidden) {
|
|
1122
|
+
element.setAttribute("hidden", "");
|
|
1123
|
+
} else {
|
|
1124
|
+
element.removeAttribute("hidden");
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function dispatchConnectivityEvent(root, eventName, status) {
|
|
1129
|
+
const detail = {
|
|
1130
|
+
status,
|
|
1131
|
+
isOnline: status === "online",
|
|
1132
|
+
isOffline: status === "offline",
|
|
1133
|
+
root
|
|
1134
|
+
};
|
|
1135
|
+
root.dispatchEvent(new CustomEvent(eventName, {
|
|
1136
|
+
bubbles: true,
|
|
1137
|
+
detail
|
|
1138
|
+
}));
|
|
1139
|
+
}
|
|
317
1140
|
exports.init = init;
|
|
1141
|
+
exports.initAltazionHtmx = initAltazionHtmx;
|
|
318
1142
|
exports.registerAltazionAuthExtension = registerAltazionAuthExtension;
|
|
1143
|
+
exports.registerAltazionExtension = registerAltazionExtension;
|
|
319
1144
|
exports.registerCartHelpers = registerCartHelpers;
|
|
320
1145
|
exports.registerPriceHelpers = registerPriceHelpers;
|
|
321
1146
|
exports.registerProductHelpers = registerProductHelpers;
|