@akinon/pz-theme 2.0.0-beta.21
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/CHANGELOG.md +17 -0
- package/package.json +27 -0
- package/readme.md +23 -0
- package/src/blocks/accordion-block.tsx +136 -0
- package/src/blocks/block-renderer-registry.tsx +77 -0
- package/src/blocks/button-block.tsx +593 -0
- package/src/blocks/counter-block.tsx +348 -0
- package/src/blocks/divider-block.tsx +20 -0
- package/src/blocks/embed-block.tsx +208 -0
- package/src/blocks/group-block.tsx +116 -0
- package/src/blocks/hotspot-block.tsx +147 -0
- package/src/blocks/icon-block.tsx +230 -0
- package/src/blocks/image-block.tsx +142 -0
- package/src/blocks/image-gallery-block.tsx +269 -0
- package/src/blocks/input-block.tsx +123 -0
- package/src/blocks/link-block.tsx +216 -0
- package/src/blocks/lottie-block.tsx +325 -0
- package/src/blocks/map-block.tsx +89 -0
- package/src/blocks/slider-block.tsx +595 -0
- package/src/blocks/tab-block.tsx +10 -0
- package/src/blocks/text-block.tsx +52 -0
- package/src/blocks/video-block.tsx +122 -0
- package/src/components/action-toolbar.tsx +305 -0
- package/src/components/designer-overlay.tsx +74 -0
- package/src/components/with-designer-features.tsx +142 -0
- package/src/dynamic-font-loader.tsx +79 -0
- package/src/hooks/use-designer-features.tsx +100 -0
- package/src/hooks/use-visibility-context.ts +27 -0
- package/src/index.ts +21 -0
- package/src/placeholder-registry.ts +31 -0
- package/src/sections/before-after-section.tsx +245 -0
- package/src/sections/contact-form-section.tsx +564 -0
- package/src/sections/countdown-campaign-banner-section.tsx +433 -0
- package/src/sections/coupon-banner-section.tsx +710 -0
- package/src/sections/divider-section.tsx +62 -0
- package/src/sections/featured-product-spotlight-section.tsx +507 -0
- package/src/sections/find-in-store-section.tsx +1995 -0
- package/src/sections/hover-showcase-section.tsx +326 -0
- package/src/sections/image-hotspot-section.tsx +142 -0
- package/src/sections/installment-options-section.tsx +1065 -0
- package/src/sections/notification-banner-section.tsx +173 -0
- package/src/sections/order-tracking-lookup-section.tsx +1379 -0
- package/src/sections/posts-slider-section.tsx +472 -0
- package/src/sections/pre-order-launch-banner-section.tsx +687 -0
- package/src/sections/section-renderer-registry.tsx +89 -0
- package/src/sections/section-wrapper.tsx +135 -0
- package/src/sections/shipping-threshold-progress-section.tsx +586 -0
- package/src/sections/stats-counter-section.tsx +486 -0
- package/src/sections/tabs-section.tsx +578 -0
- package/src/theme-block.tsx +102 -0
- package/src/theme-page-context.tsx +27 -0
- package/src/theme-placeholder-client.tsx +218 -0
- package/src/theme-placeholder-wrapper.tsx +786 -0
- package/src/theme-placeholder.tsx +305 -0
- package/src/theme-section.tsx +1241 -0
- package/src/theme-settings-context.tsx +13 -0
- package/src/utils/index.ts +791 -0
- package/src/utils/iterator-utils.test.ts +224 -0
- package/src/utils/iterator-utils.ts +617 -0
- package/src/utils/page-context-discovery.ts +119 -0
- package/src/utils/publish-window.ts +86 -0
- package/src/utils/visibility-rules.ts +188 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { Block } from '../theme-block';
|
|
2
|
+
|
|
3
|
+
interface BindingContext {
|
|
4
|
+
sectionDataSource?: any;
|
|
5
|
+
pageContext?: Record<string, unknown> | null;
|
|
6
|
+
isDesigner?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface IteratorOptions extends BindingContext {
|
|
10
|
+
iteratorBlock: Block;
|
|
11
|
+
forceIteratorCount?: number;
|
|
12
|
+
forceIteratorOffset?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const PAGE_CONTEXT_DATA_SOURCE_TYPE = 'page-context';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_BINDING_TARGETS: Record<string, string> = {
|
|
18
|
+
text: 'value',
|
|
19
|
+
image: 'value',
|
|
20
|
+
link: 'href',
|
|
21
|
+
button: 'value',
|
|
22
|
+
input: 'placeholder'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
26
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
27
|
+
|
|
28
|
+
const getValueAtPath = (source: unknown, path: string): unknown => {
|
|
29
|
+
if (!path.trim()) return source;
|
|
30
|
+
|
|
31
|
+
const tokens = path.match(/[^.[\]]+|\[(\d+)\]/g) || [];
|
|
32
|
+
let value: unknown = source;
|
|
33
|
+
|
|
34
|
+
for (const token of tokens) {
|
|
35
|
+
if (value === undefined || value === null) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (token.startsWith('[') && token.endsWith(']')) {
|
|
40
|
+
const index = Number.parseInt(token.slice(1, -1), 10);
|
|
41
|
+
value = Array.isArray(value) ? value[index] : undefined;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
value = (value as Record<string, unknown>)?.[token];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getItemBindingTarget = (block: Block): string => {
|
|
52
|
+
const explicitTarget = block.properties?.dataBindingTarget;
|
|
53
|
+
if (typeof explicitTarget === 'string' && explicitTarget.trim()) {
|
|
54
|
+
return explicitTarget.trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (block.properties?.tag === 'a') {
|
|
58
|
+
return 'href';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return DEFAULT_BINDING_TARGETS[block.type] || 'value';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const getCollectionBindingRoot = (
|
|
65
|
+
sectionDataSource: any,
|
|
66
|
+
isDesigner: boolean
|
|
67
|
+
): Record<string, unknown> | null => {
|
|
68
|
+
if (!sectionDataSource?.details?.collection) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isEditorMode =
|
|
73
|
+
isDesigner && typeof window !== 'undefined' && window.parent !== window;
|
|
74
|
+
const collection = sectionDataSource.details.collection;
|
|
75
|
+
const candidates = isEditorMode
|
|
76
|
+
? [collection?.products, collection?.data]
|
|
77
|
+
: [collection?.data, collection?.products];
|
|
78
|
+
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
if (Array.isArray(candidate)) {
|
|
81
|
+
return {
|
|
82
|
+
products: candidate,
|
|
83
|
+
items: candidate,
|
|
84
|
+
data: candidate
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isRecord(candidate)) {
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const getStaticBindingRoot = (
|
|
97
|
+
sectionDataSource: any
|
|
98
|
+
): Record<string, unknown> | null => {
|
|
99
|
+
const staticSource = sectionDataSource?.details?.static;
|
|
100
|
+
if (!staticSource) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
items: staticSource.data || [],
|
|
106
|
+
data: staticSource.data || [],
|
|
107
|
+
name: staticSource.name
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const getSectionBindingRoot = ({
|
|
112
|
+
sectionDataSource,
|
|
113
|
+
pageContext,
|
|
114
|
+
isDesigner = false
|
|
115
|
+
}: BindingContext): Record<string, unknown> | null => {
|
|
116
|
+
if (
|
|
117
|
+
sectionDataSource?.type === PAGE_CONTEXT_DATA_SOURCE_TYPE &&
|
|
118
|
+
isRecord(pageContext)
|
|
119
|
+
) {
|
|
120
|
+
return pageContext;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const collectionRoot = getCollectionBindingRoot(
|
|
124
|
+
sectionDataSource,
|
|
125
|
+
isDesigner
|
|
126
|
+
);
|
|
127
|
+
if (collectionRoot) {
|
|
128
|
+
return collectionRoot;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const staticRoot = getStaticBindingRoot(sectionDataSource);
|
|
132
|
+
if (staticRoot) {
|
|
133
|
+
return staticRoot;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const normalizeScalarValue = (
|
|
140
|
+
value: unknown,
|
|
141
|
+
target: string,
|
|
142
|
+
blockType: string
|
|
143
|
+
): string | undefined => {
|
|
144
|
+
if (value == null) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (typeof value === 'string') {
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
153
|
+
return String(value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!isRecord(value)) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (target === 'href' || target === 'url' || blockType === 'image') {
|
|
165
|
+
const urlCandidate = [value.url, value.absolute_url, value.image].find(
|
|
166
|
+
(candidate) => typeof candidate === 'string'
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (typeof urlCandidate === 'string') {
|
|
170
|
+
return urlCandidate;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const scalarCandidate = [
|
|
175
|
+
value.title,
|
|
176
|
+
value.name,
|
|
177
|
+
value.label,
|
|
178
|
+
value.value,
|
|
179
|
+
value.alt,
|
|
180
|
+
value.description,
|
|
181
|
+
value.price,
|
|
182
|
+
value.retailPrice,
|
|
183
|
+
value.url,
|
|
184
|
+
value.absolute_url
|
|
185
|
+
].find(
|
|
186
|
+
(candidate) =>
|
|
187
|
+
typeof candidate === 'string' ||
|
|
188
|
+
typeof candidate === 'number' ||
|
|
189
|
+
typeof candidate === 'boolean'
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
typeof scalarCandidate === 'string' ||
|
|
194
|
+
typeof scalarCandidate === 'number' ||
|
|
195
|
+
typeof scalarCandidate === 'boolean'
|
|
196
|
+
) {
|
|
197
|
+
return String(scalarCandidate);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return undefined;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const parsePriceValue = (value: unknown): number => {
|
|
204
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
205
|
+
if (typeof value !== 'string') return 0;
|
|
206
|
+
|
|
207
|
+
const cleaned = value.trim().replace(/[^\d,.-]/g, '');
|
|
208
|
+
if (!cleaned) return 0;
|
|
209
|
+
|
|
210
|
+
let normalized = cleaned;
|
|
211
|
+
const hasComma = normalized.includes(',');
|
|
212
|
+
const hasDot = normalized.includes('.');
|
|
213
|
+
|
|
214
|
+
if (hasComma && hasDot) {
|
|
215
|
+
const lastComma = normalized.lastIndexOf(',');
|
|
216
|
+
const lastDot = normalized.lastIndexOf('.');
|
|
217
|
+
normalized =
|
|
218
|
+
lastComma > lastDot
|
|
219
|
+
? normalized.replace(/\./g, '').replace(',', '.')
|
|
220
|
+
: normalized.replace(/,/g, '');
|
|
221
|
+
} else if (hasComma) {
|
|
222
|
+
const unsigned = normalized.replace(/^-/, '');
|
|
223
|
+
const isThousandsPattern = /^\d{1,3}(,\d{3})+$/.test(unsigned);
|
|
224
|
+
normalized = isThousandsPattern
|
|
225
|
+
? normalized.replace(/,/g, '')
|
|
226
|
+
: normalized.replace(/,/g, '.');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const parsed = Number(normalized);
|
|
230
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const escapeHtml = (value: string): string =>
|
|
234
|
+
value
|
|
235
|
+
.replace(/&/g, '&')
|
|
236
|
+
.replace(/</g, '<')
|
|
237
|
+
.replace(/>/g, '>')
|
|
238
|
+
.replace(/"/g, '"')
|
|
239
|
+
.replace(/'/g, ''');
|
|
240
|
+
|
|
241
|
+
const formatDisplayPrice = (value: unknown): string => {
|
|
242
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
243
|
+
const hasDecimals = Math.abs(value % 1) > 0.00001;
|
|
244
|
+
return value.toLocaleString('tr-TR', {
|
|
245
|
+
minimumFractionDigits: hasDecimals ? 2 : 0,
|
|
246
|
+
maximumFractionDigits: 2
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (typeof value !== 'string') return '0';
|
|
251
|
+
const raw = value.trim();
|
|
252
|
+
if (!raw) return '0';
|
|
253
|
+
if (/[A-Za-z]/.test(raw)) return raw;
|
|
254
|
+
|
|
255
|
+
const parsed = parsePriceValue(raw);
|
|
256
|
+
const hasDecimals = Math.abs(parsed % 1) > 0.00001;
|
|
257
|
+
return parsed.toLocaleString('tr-TR', {
|
|
258
|
+
minimumFractionDigits: hasDecimals ? 2 : 0,
|
|
259
|
+
maximumFractionDigits: 2
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const normalizeCurrencyLabel = (rawCurrency: unknown): string => {
|
|
264
|
+
if (typeof rawCurrency !== 'string') return 'TL';
|
|
265
|
+
const normalized = rawCurrency.trim().toUpperCase();
|
|
266
|
+
if (!normalized || normalized === 'TRY' || normalized === 'TL') return 'TL';
|
|
267
|
+
return normalized;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const getPriceWithCurrency = (value: unknown, currency: unknown): string => {
|
|
271
|
+
const amount = formatDisplayPrice(value);
|
|
272
|
+
const currencyLabel = normalizeCurrencyLabel(currency);
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
amount.toUpperCase().includes(currencyLabel) ||
|
|
276
|
+
/[A-Za-z]{2,}/.test(amount)
|
|
277
|
+
) {
|
|
278
|
+
return amount;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return `${amount} ${currencyLabel}`;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const buildProductPriceHtml = (
|
|
285
|
+
productData: Record<string, unknown>
|
|
286
|
+
): string => {
|
|
287
|
+
const activePrice =
|
|
288
|
+
((productData as any)?.active_price as
|
|
289
|
+
| Record<string, unknown>
|
|
290
|
+
| undefined) || {};
|
|
291
|
+
const currentRaw = activePrice?.price ?? (productData as any)?.price;
|
|
292
|
+
const retailRaw =
|
|
293
|
+
activePrice?.retail_price ?? (productData as any)?.retail_price;
|
|
294
|
+
const currency =
|
|
295
|
+
activePrice?.currency_type ??
|
|
296
|
+
(productData as any)?.currency_type ??
|
|
297
|
+
(productData as any)?.currency;
|
|
298
|
+
|
|
299
|
+
const currentNumeric = parsePriceValue(currentRaw);
|
|
300
|
+
const retailNumeric = parsePriceValue(retailRaw);
|
|
301
|
+
|
|
302
|
+
const currentText = escapeHtml(getPriceWithCurrency(currentRaw, currency));
|
|
303
|
+
const retailText = escapeHtml(getPriceWithCurrency(retailRaw, currency));
|
|
304
|
+
|
|
305
|
+
if (retailNumeric > 0 && retailNumeric > currentNumeric) {
|
|
306
|
+
return `<span style="display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;"><span style='color:#6b7280;text-decoration:line-through;font-weight:400;'>${retailText}</span><span style='font-weight:600;'>${currentText}</span></span>`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return currentText;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const isPriceBinding = (dataBinding: string): boolean => {
|
|
313
|
+
const path = dataBinding.replace('item.', '');
|
|
314
|
+
return (
|
|
315
|
+
path === 'active_price.price' || path === 'price' || path === 'retail_price'
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const applyResolvedBinding = (
|
|
320
|
+
block: Block,
|
|
321
|
+
resolvedValue: unknown,
|
|
322
|
+
bindingPath: string
|
|
323
|
+
): Block => {
|
|
324
|
+
const target = getItemBindingTarget(block);
|
|
325
|
+
const nextBlock: Block = {
|
|
326
|
+
...block,
|
|
327
|
+
properties: block.properties ? { ...block.properties } : block.properties
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (
|
|
331
|
+
target === 'value' &&
|
|
332
|
+
block.type === 'text' &&
|
|
333
|
+
bindingPath.startsWith('item.') &&
|
|
334
|
+
isPriceBinding(bindingPath) &&
|
|
335
|
+
isRecord(resolvedValue)
|
|
336
|
+
) {
|
|
337
|
+
nextBlock.value = buildProductPriceHtml(resolvedValue);
|
|
338
|
+
return nextBlock;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const normalizedValue = normalizeScalarValue(
|
|
342
|
+
resolvedValue,
|
|
343
|
+
target,
|
|
344
|
+
block.type
|
|
345
|
+
);
|
|
346
|
+
if (normalizedValue === undefined) {
|
|
347
|
+
return nextBlock;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
switch (target) {
|
|
351
|
+
case 'href':
|
|
352
|
+
nextBlock.properties = {
|
|
353
|
+
...(nextBlock.properties || {}),
|
|
354
|
+
href: normalizedValue
|
|
355
|
+
};
|
|
356
|
+
return nextBlock;
|
|
357
|
+
|
|
358
|
+
case 'url':
|
|
359
|
+
nextBlock.properties = {
|
|
360
|
+
...(nextBlock.properties || {}),
|
|
361
|
+
url: normalizedValue
|
|
362
|
+
};
|
|
363
|
+
return nextBlock;
|
|
364
|
+
|
|
365
|
+
case 'placeholder':
|
|
366
|
+
nextBlock.properties = {
|
|
367
|
+
...(nextBlock.properties || {}),
|
|
368
|
+
placeholder: normalizedValue
|
|
369
|
+
};
|
|
370
|
+
return nextBlock;
|
|
371
|
+
|
|
372
|
+
case 'alt':
|
|
373
|
+
nextBlock.properties = {
|
|
374
|
+
...(nextBlock.properties || {}),
|
|
375
|
+
alt: normalizedValue
|
|
376
|
+
};
|
|
377
|
+
return nextBlock;
|
|
378
|
+
|
|
379
|
+
default:
|
|
380
|
+
nextBlock.value = normalizedValue;
|
|
381
|
+
return nextBlock;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export const resolveBlockBindings = ({
|
|
386
|
+
block,
|
|
387
|
+
sectionDataSource,
|
|
388
|
+
pageContext,
|
|
389
|
+
isDesigner = false
|
|
390
|
+
}: BindingContext & { block: Block }): Block => {
|
|
391
|
+
const bindingRoot = getSectionBindingRoot({
|
|
392
|
+
sectionDataSource,
|
|
393
|
+
pageContext,
|
|
394
|
+
isDesigner
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const walk = (sourceBlock: Block): Block => {
|
|
398
|
+
const nextBlock: Block = {
|
|
399
|
+
...sourceBlock,
|
|
400
|
+
properties: sourceBlock.properties
|
|
401
|
+
? { ...sourceBlock.properties }
|
|
402
|
+
: sourceBlock.properties
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const bindingPath =
|
|
406
|
+
typeof nextBlock.properties?.dataBinding === 'string'
|
|
407
|
+
? nextBlock.properties.dataBinding.trim()
|
|
408
|
+
: '';
|
|
409
|
+
|
|
410
|
+
let resolvedBlock = nextBlock;
|
|
411
|
+
if (bindingRoot && bindingPath && !bindingPath.startsWith('item.')) {
|
|
412
|
+
const resolvedValue = getValueAtPath(bindingRoot, bindingPath);
|
|
413
|
+
if (resolvedValue !== undefined) {
|
|
414
|
+
resolvedBlock = applyResolvedBinding(
|
|
415
|
+
nextBlock,
|
|
416
|
+
resolvedValue,
|
|
417
|
+
bindingPath
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (resolvedBlock.blocks && resolvedBlock.blocks.length > 0) {
|
|
423
|
+
resolvedBlock.blocks = resolvedBlock.blocks.map(walk);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return resolvedBlock;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
return walk(block);
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const parsePositiveNumber = (value: unknown, fallback: number): number => {
|
|
433
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
434
|
+
return Math.max(0, Math.floor(value));
|
|
435
|
+
}
|
|
436
|
+
if (typeof value === 'string') {
|
|
437
|
+
const parsed = Number(value);
|
|
438
|
+
if (Number.isFinite(parsed)) {
|
|
439
|
+
return Math.max(0, Math.floor(parsed));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return fallback;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const getIteratorItems = ({
|
|
446
|
+
sectionDataSource,
|
|
447
|
+
dataPath,
|
|
448
|
+
isDesigner = false,
|
|
449
|
+
pageContext
|
|
450
|
+
}: BindingContext & { dataPath?: string }): Record<string, unknown>[] => {
|
|
451
|
+
const bindingRoot = getSectionBindingRoot({
|
|
452
|
+
sectionDataSource,
|
|
453
|
+
pageContext,
|
|
454
|
+
isDesigner
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (!bindingRoot) {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (dataPath) {
|
|
462
|
+
const resolvedValue = getValueAtPath(bindingRoot, dataPath);
|
|
463
|
+
return Array.isArray(resolvedValue)
|
|
464
|
+
? (resolvedValue as Record<string, unknown>[])
|
|
465
|
+
: [];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (Array.isArray((bindingRoot as any).items)) {
|
|
469
|
+
return (bindingRoot as any).items;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (Array.isArray((bindingRoot as any).products)) {
|
|
473
|
+
return (bindingRoot as any).products;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return [];
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const replaceBlockValues = (
|
|
480
|
+
blockToReplace: Block,
|
|
481
|
+
itemData: Record<string, unknown>,
|
|
482
|
+
itemIndex: number
|
|
483
|
+
): Block => {
|
|
484
|
+
const nextBlock: Block = {
|
|
485
|
+
...blockToReplace,
|
|
486
|
+
properties: blockToReplace.properties
|
|
487
|
+
? { ...blockToReplace.properties }
|
|
488
|
+
: blockToReplace.properties
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const isPlaceholderMode = !itemData || Object.keys(itemData).length === 0;
|
|
492
|
+
const bindingPath =
|
|
493
|
+
typeof nextBlock.properties?.dataBinding === 'string'
|
|
494
|
+
? nextBlock.properties.dataBinding.trim()
|
|
495
|
+
: '';
|
|
496
|
+
|
|
497
|
+
if (bindingPath && !isPlaceholderMode) {
|
|
498
|
+
const itemBindingPath = bindingPath.replace(/^item\./, '');
|
|
499
|
+
const resolvedValue = getValueAtPath(itemData, itemBindingPath);
|
|
500
|
+
|
|
501
|
+
if (resolvedValue !== undefined) {
|
|
502
|
+
if (
|
|
503
|
+
nextBlock.type === 'text' &&
|
|
504
|
+
bindingPath.startsWith('item.') &&
|
|
505
|
+
isPriceBinding(bindingPath)
|
|
506
|
+
) {
|
|
507
|
+
nextBlock.value = buildProductPriceHtml(itemData);
|
|
508
|
+
} else {
|
|
509
|
+
return {
|
|
510
|
+
...applyResolvedBinding(nextBlock, resolvedValue, bindingPath),
|
|
511
|
+
blocks: nextBlock.blocks?.map((childBlock) =>
|
|
512
|
+
replaceBlockValues(childBlock, itemData, itemIndex)
|
|
513
|
+
)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} else if (isPlaceholderMode && nextBlock.type === 'image') {
|
|
518
|
+
const placeholderImages = [
|
|
519
|
+
'/assets/images/product-placeholder-1.jpg',
|
|
520
|
+
'/assets/images/product-placeholder-2.jpg',
|
|
521
|
+
'/assets/images/product-placeholder-3.jpg',
|
|
522
|
+
'/assets/images/product-placeholder-4.jpg'
|
|
523
|
+
];
|
|
524
|
+
|
|
525
|
+
const placeholderIndex = itemIndex % placeholderImages.length;
|
|
526
|
+
|
|
527
|
+
if (
|
|
528
|
+
nextBlock.value &&
|
|
529
|
+
typeof nextBlock.value === 'string' &&
|
|
530
|
+
nextBlock.value.includes('product-placeholder')
|
|
531
|
+
) {
|
|
532
|
+
nextBlock.value = placeholderImages[placeholderIndex];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (nextBlock.blocks && nextBlock.blocks.length > 0) {
|
|
537
|
+
nextBlock.blocks = nextBlock.blocks.map((childBlock) =>
|
|
538
|
+
replaceBlockValues(childBlock, itemData, itemIndex)
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return nextBlock;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
export const buildIteratorBlock = ({
|
|
546
|
+
iteratorBlock,
|
|
547
|
+
sectionDataSource,
|
|
548
|
+
pageContext,
|
|
549
|
+
isDesigner = false,
|
|
550
|
+
forceIteratorCount,
|
|
551
|
+
forceIteratorOffset
|
|
552
|
+
}: IteratorOptions): Block => {
|
|
553
|
+
if (
|
|
554
|
+
!iteratorBlock.isIterator ||
|
|
555
|
+
!iteratorBlock.blocks ||
|
|
556
|
+
iteratorBlock.blocks.length === 0
|
|
557
|
+
) {
|
|
558
|
+
return iteratorBlock;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const template = iteratorBlock.blocks[0];
|
|
562
|
+
const dataPath =
|
|
563
|
+
iteratorBlock.iteratorDataPath ||
|
|
564
|
+
iteratorBlock.properties?.iteratorDataPath;
|
|
565
|
+
const items = getIteratorItems({
|
|
566
|
+
sectionDataSource,
|
|
567
|
+
pageContext,
|
|
568
|
+
dataPath,
|
|
569
|
+
isDesigner
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const useIteratorCount =
|
|
573
|
+
forceIteratorCount !== undefined
|
|
574
|
+
? true
|
|
575
|
+
: iteratorBlock.properties?.useIteratorCount === true ||
|
|
576
|
+
iteratorBlock.properties?.useIteratorCount === 'true';
|
|
577
|
+
const iteratorCount =
|
|
578
|
+
forceIteratorCount !== undefined
|
|
579
|
+
? Math.max(1, Math.floor(forceIteratorCount))
|
|
580
|
+
: parsePositiveNumber(iteratorBlock.properties?.iteratorCount, 1);
|
|
581
|
+
const iteratorOffset =
|
|
582
|
+
forceIteratorOffset !== undefined
|
|
583
|
+
? Math.max(0, Math.floor(forceIteratorOffset))
|
|
584
|
+
: parsePositiveNumber(iteratorBlock.properties?.iteratorOffset, 0);
|
|
585
|
+
|
|
586
|
+
const displayItems =
|
|
587
|
+
items.length > 0
|
|
588
|
+
? useIteratorCount
|
|
589
|
+
? items.slice(iteratorOffset, iteratorOffset + iteratorCount)
|
|
590
|
+
: items
|
|
591
|
+
: [];
|
|
592
|
+
|
|
593
|
+
const actualCount =
|
|
594
|
+
displayItems.length > 0 ? displayItems.length : Math.max(iteratorCount, 1);
|
|
595
|
+
|
|
596
|
+
const clonedBlocks = Array.from({ length: actualCount }, (_, index) => {
|
|
597
|
+
const item = displayItems[index] || {};
|
|
598
|
+
|
|
599
|
+
const templateCopy: Block = {
|
|
600
|
+
...template,
|
|
601
|
+
id: `${template.id}-clone-${index}`,
|
|
602
|
+
properties: template.properties
|
|
603
|
+
? { ...template.properties }
|
|
604
|
+
: template.properties,
|
|
605
|
+
blocks: template.blocks
|
|
606
|
+
? template.blocks.map((childBlock) => ({ ...childBlock }))
|
|
607
|
+
: undefined
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
return replaceBlockValues(templateCopy, item, index);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
...iteratorBlock,
|
|
615
|
+
blocks: clonedBlocks
|
|
616
|
+
};
|
|
617
|
+
};
|