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