PageSourceSearch

https://www.lojaintt.com.br/common/js/cartGamificationBanner.js?v=1

js lojaintt.com.br collected 2026-09-26 10:09:32 UTC 26,109 bytes, 754 lines download raw bytes

1var cartGamificationBanner = [];
2var cartGamificationBannerItems = null;
3
4var CGB_CAROUSEL_INTERVAL_MS = 4000;
5var CGB_VARIANTS = ['', 'OnCart', 'OnProduct', 'OnProductModal'];
6
7var cgbBannersByVariant = {};
8
9function cgbGetEl(selector, parent) {
10	return (parent || document).querySelector(selector);
11}
12
13function cgbGetAllEls(selector, parent) {
14	return (parent || document).querySelectorAll(selector);
15}
16
17function cgbSetText(selector, value) {
18	var element = cgbGetEl(selector);
19	if (element && value !== undefined && value !== null) {
20		element.textContent = value;
21	}
22}
23
24cartGamificationBanner.parseStringToFloat = function (value) {
25	return parseFloat(
26		String(value)
27			.replace(/ /g, ' ')
28			.replace('R$ ', '')
29			.replace(/\./g, '')
30			.replace(',', '.')
31	);
32};
33
34cartGamificationBanner.parseStringToInteger = function (value) {
35	return parseInt(
36		String(value).replace('R$ ', '').replace(/\./g, '').replace(',', '.'),
37		10
38	);
39};
40
41cartGamificationBanner.parseFloatToCurrency = function (value) {
42	return value
43		.toLocaleString('pt-br', { style: 'currency', currency: 'BRL' })
44		.replace('R$', '');
45};
46
47cartGamificationBanner.padNumber = function (number) {
48	return number < 10 ? '0' + number : String(number);
49};
50
51cartGamificationBanner.isAppContext = function () {
52	return (
53		window.location.hostname.indexOf('app.') !== -1 ||
54		typeof window.flutter_inappwebview !== 'undefined'
55	);
56};
57
58function cgbShouldShowOnThisContext(banner) {
59	var exibicao = banner.exibicao;
60	if (!exibicao || exibicao === 'ambos') return true;
61	if (exibicao === 'app') return cartGamificationBanner.isAppContext();
62	if (exibicao === 'site') return !cartGamificationBanner.isAppContext();
63	return true;
64}
65
66cartGamificationBanner.encodeCartGamificationBannerName = function (banner) {
67	var decoder = document.createElement('textarea');
68	decoder.innerHTML = banner.nome;
69	banner.nome = decoder.value
70		.normalize('NFD')
71		.replace(/[̀-ͯ]/g, '')
72		.replace(/[^a-z0-9]/gi, '');
73	return banner;
74};
75
76/**
77 * Simula em cima de `subtotal` o efeito de uma regra de pagamento (PIX 10% off,
78 * boleto 5% off, etc.) sem precisar consultar o backend. Permite que o banner
79 * mostre o "preço final" estimado já durante a navegação.
80 *
81 * @param {number} subtotal       Valor base (já em reais, número).
82 * @param {object} regraPagamento Objeto `{ valor, tipo, aplicacao }`:
83 *   - `valor`: número > 0 (porcentagem ou valor fixo)
84 *   - `tipo`: "porcentagem" | "percentual" | "valor"
85 *   - `aplicacao`: "desconto" (subtrai) | "acrescimo" (soma)
86 * @returns {number} O subtotal já com a regra aplicada. Devolve o `subtotal`
87 *                   original quando a regra está mal formada.
88 */
89cartGamificationBanner.aplicarRegraPagamento = function (subtotal, regraPagamento) {
90	if (!regraPagamento || typeof regraPagamento !== 'object') {
91		return subtotal;
92	}
93
94	var fator = parseFloat(regraPagamento.valor);
95	if (isNaN(fator) || fator <= 0) {
96		return subtotal;
97	}
98
99	var tipo = String(regraPagamento.tipo || '').toLowerCase();
100	var aplicacao = String(regraPagamento.aplicacao || 'desconto').toLowerCase();
101	var sinal = aplicacao === 'acrescimo' ? 1 : -1;
102
103	if (tipo === 'porcentagem' || tipo === 'percentual') {
104		return subtotal * (1 + sinal * (fator / 100));
105	}
106
107	return subtotal + sinal * fator;
108};
109
110/**
111 * Lê o JSON do banner que o backend imprimiu dentro do `<div cartGamificationBannerItems…>`.
112 * O backend concatena vários banners separados por vírgula (`{…},{…},`); para virar um
113 * array JSON válido, removemos a vírgula final e embrulhamos em `[ … ]`.
114 *
115 * @param {string} variant Sufixo da variante ("", "OnCart", "OnProduct", "OnProductModal").
116 * @returns {Array<object>|null} Lista de banners parseados, ou null se vazio/inválido.
117 */
118function cgbParseJsonFromContainer(variant) {
119	var container = cgbGetEl('[cartGamificationBannerItems' + variant + ']');
120	if (!container) return null;
121
122	var raw = (container.innerHTML || '').trim();
123	if (!raw) return null;
124
125	if (raw.charAt(raw.length - 1) === ',') {
126		raw = raw.slice(0, -1);
127	}
128
129	try {
130		return JSON.parse('[' + raw + ']');
131	} catch (err) {
132		console.error('cartGamificationBanner: JSON inválido', err);
133		return null;
134	}
135}
136
137cartGamificationBanner.bannerItemHtml = function (banner, element) {
138	var variant = element || '';
139
140	var iconHtml = banner.textoIcone
141		? '<img class="cart-gamification-banner__icon" src="' + banner.textoIcone + '" alt="gamification" />'
142		: '';
143
144	var fontSize = banner.tamanhoDaFonte ? 'font-size: ' + banner.tamanhoDaFonte + ';' : '';
145	var fontWeight = banner.pesoDaFonte ? 'font-weight: ' + banner.pesoDaFonte + ';' : '';
146	var barHeight = banner.tamanhoBarraProgresso ? 'height: ' + banner.tamanhoBarraProgresso + ';' : '';
147	var barPosition = banner.posicaoBarraProgresso ? banner.posicaoBarraProgresso + ': 0;' : '';
148
149	var countdownHtml = '';
150	if (banner.cronometroData && banner.cronometroHora) {
151		countdownHtml =
152			'<span class="cart-gamification-banner__countdown" ' +
153				'cartGamificationBannerCountdown' + variant + ' ' +
154				'data-date="' + banner.cronometroData + '" ' +
155				'data-time="' + banner.cronometroHora + '">' +
156			'</span>';
157	}
158
159	var progressStyle = banner.imagemDoProgresso
160		? "background-image: url('" + banner.imagemDoProgresso + "');"
161		: 'background: ' + (banner.corDoProgresso || 'transparent') + ';';
162
163	var initialStyle = banner.corDoFundo
164		? 'background: ' + banner.corDoFundo + ';'
165		: "background-image: url('" + (banner.imagemDeFundo || '') + "');";
166
167	return (
168		'<div class="cart-gamification-banner__item" cartGamificationBannerName' + variant + '=' + banner.nome + '>' +
169			'<div class="cart-gamification-banner__text" style="color: ' + (banner.corDaFonte || '') + '; ' + fontSize + ' ' + fontWeight + '">' +
170				iconHtml +
171				'<span cartGamificationBannerText' + variant + '>' + banner.textoInicial + '</span>' +
172				countdownHtml +
173			'</div>' +
174			'<div class="cart-gamification-banner__progress-background" ' +
175				'style="' + progressStyle + ' width: 0%; ' + barHeight + ' ' + barPosition + '" ' +
176				'cartGamificationBannerProgress' + variant + '></div>' +
177			'<div class="cart-gamification-banner__initial-background" style="' + initialStyle + '"></div>' +
178		'</div>'
179	);
180};
181
182cartGamificationBanner.generateBanners = function (element) {
183	var variant = element || '';
184
185	var parsedFromDom = cgbParseJsonFromContainer(variant);
186	if (parsedFromDom) {
187		cgbBannersByVariant[variant] = parsedFromDom;
188		if (variant === '') {
189			cartGamificationBannerItems = parsedFromDom;
190		}
191	}
192
193	var banners = cgbBannersByVariant[variant];
194	if (!banners || !banners.length) {
195		return;
196	}
197
198	var html = '';
199	for (var i = 0; i < banners.length; i++) {
200		var banner = banners[i];
201		if (!cgbShouldShowOnThisContext(banner)) continue;
202
203		banner = cartGamificationBanner.encodeCartGamificationBannerName(banner);
204		html += cartGamificationBanner.bannerItemHtml(banner, variant);
205	}
206
207	var container = cgbGetEl('[cartGamificationBannerContainer' + variant + ']');
208	if (!container) return;
209
210	container.innerHTML = html;
211	container.classList.add('cart-gamification-banner__active');
212
213	cartGamificationBanner.initializeCarousel(variant);
214	cartGamificationBanner.initCountdowns(variant);
215
216	if (typeof cart !== 'undefined' && cart.subtotal !== undefined) {
217		cartGamificationBanner.calculateBannerProgressBarOnCheckout(variant);
218	} else {
219		cartGamificationBanner.calculateBannerProgressBar(variant);
220	}
221};
222
223cartGamificationBanner.initializeCarousel = function (element) {
224	var variant = element || '';
225	var bannerItems = cgbGetAllEls('[cartGamificationBannerName' + variant + ']');
226
227	if (bannerItems.length < 2) return;
228
229	var firstItem = bannerItems[0];
230	var itemHeight = firstItem.offsetHeight;
231	var currentIndex = 0;
232	var totalItems = bannerItems.length;
233
234	setInterval(function () {
235		currentIndex = (currentIndex + 1) % totalItems;
236		firstItem.style.marginTop = '-' + currentIndex * itemHeight + 'px';
237	}, CGB_CAROUSEL_INTERVAL_MS);
238};
239
240cartGamificationBanner.parseCronometroDate = function (dateStr, timeStr) {
241	var dateParts = String(dateStr).split('/');
242	var timeParts = String(timeStr).split(':');
243	if (dateParts.length !== 3 || timeParts.length !== 3) return null;
244
245	var targetDate = new Date(
246		+dateParts[2], +dateParts[1] - 1, +dateParts[0],
247		+timeParts[0], +timeParts[1], +timeParts[2]
248	);
249	return isNaN(targetDate.getTime()) ? null : targetDate;
250};
251
252cartGamificationBanner.initCountdowns = function (element) {
253	var variant = element || '';
254	var countdownEls = cgbGetAllEls('[cartGamificationBannerCountdown' + variant + ']');
255
256	for (var i = 0; i < countdownEls.length; i++) {
257		cartGamificationBanner.bindCountdown(countdownEls[i]);
258	}
259};
260
261cartGamificationBanner.bindCountdown = function (countdownEl) {
262	var targetDate = cartGamificationBanner.parseCronometroDate(
263		countdownEl.getAttribute('data-date'),
264		countdownEl.getAttribute('data-time')
265	);
266	if (!targetDate) return;
267
268	if (!cartGamificationBanner.tickCountdown(countdownEl, targetDate)) return;
269
270	var intervalId = setInterval(function () {
271		if (!cartGamificationBanner.tickCountdown(countdownEl, targetDate)) {
272			clearInterval(intervalId);
273		}
274	}, 1000);
275};
276
277/**
278 * Renderiza um "tick" do cronômetro regressivo no `countdownEl`. Chamada a cada
279 * segundo pelo `bindCountdown`. Cobre 3 estados visuais:
280 *
281 *   - **Tempo restante > 60s** — renderiza `dd:hh:mm:ss`, sem destaque.
282 *   - **Tempo restante ≤ 60s** — adiciona a classe `--urgent` (CSS pulsa a barra).
283 *   - **Expirado (≤ 0)** — adiciona `--expired` no `.cart-gamification-banner__item` pai
284 *     (CSS esconde o item inteiro) e devolve `false` pro caller parar o `setInterval`.
285 *
286 * @param {Element} countdownEl  O `<span>` que recebe o markup do tempo.
287 * @param {Date}    targetDate   Data-alvo do cronômetro.
288 * @returns {boolean} `true` enquanto o cronômetro deve continuar; `false` ao expirar.
289 */
290cartGamificationBanner.tickCountdown = function (countdownEl, targetDate) {
291	var remainingMs = targetDate.getTime() - Date.now();
292
293	if (remainingMs <= 0) {
294		var bannerItem = countdownEl.closest('.cart-gamification-banner__item');
295		if (bannerItem) {
296			bannerItem.classList.add('cart-gamification-banner__item--expired');
297		}
298		countdownEl.innerHTML = '';
299		return false;
300	}
301
302	var totalSeconds = Math.floor(remainingMs / 1000);
303	var days = Math.floor(totalSeconds / 86400);
304	var hours = Math.floor((totalSeconds % 86400) / 3600);
305	var minutes = Math.floor((totalSeconds % 3600) / 60);
306	var seconds = totalSeconds % 60;
307	var pad = cartGamificationBanner.padNumber;
308
309	if (totalSeconds <= 60) {
310		countdownEl.classList.add('cart-gamification-banner__countdown--urgent');
311	} else {
312		countdownEl.classList.remove('cart-gamification-banner__countdown--urgent');
313	}
314
315	var html = '';
316	if (days > 0) {
317		html += cartGamificationBanner.renderCountdownBlock(days, 'd');
318		html += cartGamificationBanner.renderCountdownSeparator();
319	}
320	html += cartGamificationBanner.renderCountdownBlock(pad(hours), 'h');
321	html += cartGamificationBanner.renderCountdownSeparator();
322	html += cartGamificationBanner.renderCountdownBlock(pad(minutes), 'm');
323	html += cartGamificationBanner.renderCountdownSeparator();
324	html += cartGamificationBanner.renderCountdownBlock(pad(seconds), 's');
325
326	countdownEl.innerHTML = html;
327	return true;
328};
329
330cartGamificationBanner.renderCountdownBlock = function (value, label) {
331	return (
332		'<span class="cgb-countdown__block">' +
333			'<span class="cgb-countdown__value">' + value + '</span>' +
334			'<span class="cgb-countdown__label">' + label + '</span>' +
335		'</span>'
336	);
337};
338
339cartGamificationBanner.renderCountdownSeparator = function () {
340	return '<span class="cgb-countdown__sep">:</span>';
341};
342
343cartGamificationBanner.calculateBannerProgressBar = function (element) {
344	var variant = element || '';
345
346	var banners = cgbBannersByVariant[variant];
347	if (!banners || !banners.length) return;
348
349	for (var i = 0; i < banners.length; i++) {
350		cgbUpdateBannerItem(banners[i], variant);
351	}
352};
353
354function cgbUpdateBannerItem(banner, variant) {
355	var cartValues = cgbReadCartValues(banner, variant);
356	if (!cartValues) return;
357
358	if (banner.regraPagamento && banner.regra !== 'itens') {
359		cartValues.cartValue = cartGamificationBanner.aplicarRegraPagamento(
360			cartValues.cartValue,
361			banner.regraPagamento
362		);
363	}
364
365	var bannerEl = cgbGetEl('[cartGamificationBannerName' + variant + '=' + banner.nome + ']');
366	if (!bannerEl) return;
367
368	var visualState = cgbComputeVisual(banner, cartValues, variant);
369	cgbApplyVisual(bannerEl, banner, visualState, variant);
370}
371
372/**
373 * Lê do DOM os valores do carrinho que esse banner precisa para o cálculo.
374 * O conjunto de valores muda conforme a `regra`:
375 *
376 *   - `'itens'`            → contagem inteira (quantos itens vs quantos faltam)
377 *   - `'maior+itens'`      → subtotal monetário + contagem de itens (precisa dos dois)
378 *   - demais regras        → apenas subtotal monetário
379 *
380 * Devolve `null` se algum dado obrigatório não estiver no DOM, para que o caller
381 * pule esse banner sem renderizar nada incorreto.
382 *
383 * @param {object} banner   Configuração do banner vinda do JSON.
384 * @param {string} variant  Sufixo da variante para montar o seletor.
385 * @returns {object|null}   `{ cartValue, expectedValue, cartItemsValue?, expectedItemsValue? }` ou null.
386 */
387function cgbReadCartValues(banner, variant) {
388	var valueEl = cgbGetEl('[cartGamification' + variant + banner.atributo + ']');
389	if (!valueEl) return null;
390
391	var rawValue = valueEl.innerHTML;
392	if (rawValue === undefined) return null;
393
394	if (banner.regra === 'itens') {
395		return {
396			cartValue: cartGamificationBanner.parseStringToInteger(rawValue),
397			expectedValue: cartGamificationBanner.parseStringToInteger(banner.valorDesejado)
398		};
399	}
400
401	if (banner.regra === 'maior+itens') {
402		var itemsEl = cgbGetEl('[cartGamification' + variant + 'Itens]');
403		var rawItems = itemsEl ? itemsEl.innerHTML : undefined;
404		if (rawItems === undefined || banner.valorItensDesejado === undefined) return null;
405
406		return {
407			cartValue: cartGamificationBanner.parseStringToFloat(rawValue),
408			expectedValue: cartGamificationBanner.parseStringToFloat(banner.valorDesejado),
409			cartItemsValue: cartGamificationBanner.parseStringToInteger(rawItems),
410			expectedItemsValue: cartGamificationBanner.parseStringToInteger(banner.valorItensDesejado)
411		};
412	}
413
414	return {
415		cartValue: cartGamificationBanner.parseStringToFloat(rawValue),
416		expectedValue: cartGamificationBanner.parseStringToFloat(banner.valorDesejado)
417	};
418}
419
420/**
421 * Função pura que decide texto, largura e estado de "concluído" para um banner,
422 * sem tocar no DOM. Quem aplica o resultado é o `cgbApplyVisual` depois. Trabalha em 3 etapas:
423 *
424 *   1. **Carrinho vazio** (`cartValue <= 0`) → mostra `textoInicial` e barra 0%.
425 *   2. **Regra aplicada** (`cgbApplyRule`) → calcula percentual e quanto falta.
426 *   3. **Substituição de placeholders** → `{value}` (valor faltante em BRL),
427 *      `{items}` (qtde faltante), `{discount}` (cálculo entre preço por/original).
428 *
429 * Também faz o override "cliente bonificado por tabela de preço automática":
430 * mesmo sem bater a meta, marca como concluído quando o flag está ativo.
431 *
432 * @param {object} banner      Banner do JSON.
433 * @param {object} cartValues  Saída de `cgbReadCartValues`.
434 * @param {string} variant     Sufixo da variante.
435 * @returns {{text: string, width: string, finish: boolean}}
436 */
437function cgbComputeVisual(banner, cartValues, variant) {
438	var fontSize = banner.tamanhoDaFonte ? 'font-size: ' + banner.tamanhoDaFonte + ';' : '';
439	var fontWeight = banner.pesoDaFonte ? 'font-weight: ' + banner.pesoDaFonte + ';' : '';
440	var finishText = banner.linkResultado
441		? '<a href="' + banner.linkResultado + '" style="' + fontSize + ' ' + fontWeight + '">' + banner.textoResultado + '</a>'
442		: banner.textoResultado;
443
444	if (cartValues.cartValue <= 0) {
445		var initialText = banner.textoInicial;
446		if ((banner.regra || '').trim() === 'maior+itens') {
447			initialText = initialText.replace('{items}', cartValues.expectedItemsValue);
448		}
449		initialText = initialText.replace('{value}', banner.valorDesejado);
450		return { text: initialText, width: '0%', finish: false };
451	}
452
453	var ruleResult = cgbApplyRule(banner.regra, cartValues);
454
455	if (banner.atributo === 'precoportabeladeprecoautomatica') {
456		var bonusFlagEl = cgbGetEl('[cartGamification' + variant + 'ClienteBonificadoTabelaDePrecoAutomatica]');
457		if (bonusFlagEl && bonusFlagEl.innerHTML === 'true') {
458			ruleResult.finish = true;
459			ruleResult.width = '100%';
460		}
461	}
462
463	var text = ruleResult.finish ? finishText : banner.textoAndamento;
464
465	if (
466		banner.atributo === 'precopororiginal' ||
467		banner.atributo === 'precoportabeladeprecoautomatica'
468	) {
469		var currentPriceEl = cgbGetEl('[cartGamification' + variant + 'PrecoPorTabelaDePre
469coAutomatica]');
470		var originalPriceEl = cgbGetEl('[cartGamification' + variant + 'PrecoPorOriginal]');
471		if (currentPriceEl && originalPriceEl) {
472			var currentPrice = cartGamificationBanner.parseStringToFloat(currentPriceEl.innerHTML);
473			var originalPrice = cartGamificationBanner.parseStringToFloat(originalPriceEl.innerHTML);
474			text = text.replace('{discount}', cartGamificationBanner.parseFloatToCurrency(originalPrice - currentPrice));
475		}
476	}
477
478	if (!ruleResult.finish) {
479		var missingLabel = banner.atributo === 'itens'
480			? ruleResult.missingValue
481			: cartGamificationBanner.parseFloatToCurrency(ruleResult.missingValue);
482
483		if (banner.regra === 'maior+itens') {
484			text = text.replace('{items}', ruleResult.missingItems);
485		}
486		text = text.replace('{value}', missingLabel);
487	}
488
489	return { text: text, width: ruleResult.width, finish: ruleResult.finish };
490}
491
492/**
493 * Aplica a regra (`maior`, `menor`, `maior+itens` ou default) e devolve quanto
494 * falta + qual a largura percentual da barra.
495 *
496 *   - `maior`        → atinge meta quando subtotal ≥ valorDesejado
497 *   - `menor`        → atinge meta quando subtotal ≤ valorDesejado (ex.: desafio inverso)
498 *   - `maior+itens`  → precisa bater AMBOS subtotal E contagem; o percentual usa o mais distante
499 *   - default        → mesma lógica de `maior`
500 *
501 * @param {string} rule        Identificador da regra.
502 * @param {object} cartValues  Valores lidos do DOM (`cartValue`, `expectedValue`, etc.).
503 * @returns {{finish: boolean, width: string, missingValue?: number, missingItems?: number}}
504 */
505function cgbApplyRule(rule, cartValues) {
506	var cartValue = cartValues.cartValue;
507	var expected = cartValues.expectedValue;
508	var cartItems = cartValues.cartItemsValue;
509	var expectedItems = cartValues.expectedItemsValue;
510
511	if (rule === 'maior') {
512		if (cartValue >= expected) return { finish: true, width: '100%' };
513		return {
514			finish: false,
515			width: ((cartValue * 100) / expected) + '%',
516			missingValue: expected - cartValue
517		};
518	}
519
520	if (rule === 'menor') {
521		if (cartValue <= expected) return { finish: true, width: '100%' };
522		return {
523			finish: false,
524			width: ((expected * 100) / cartValue) + '%',
525			missingValue: cartValue - expected
526		};
527	}
528
529	if (rule === 'maior+itens') {
530		if (cartValue >= expected && cartItems >= expectedItems) {
531			return { finish: true, width: '100%' };
532		}
533		var missingValue = Math.max(expected - cartValue, 0);
534		var missingItems = Math.max(expectedItems - cartItems, 0);
535		var percent = missingValue > 0
536			? (cartValue * 100) / expected
537			: (cartItems * 100) / expectedItems;
538		return {
539			finish: false,
540			width: percent + '%',
541			missingValue: missingValue,
542			missingItems: missingItems
543		};
544	}
545
546	if (cartValue >= expected) return { finish: true, width: '100%' };
547	return {
548		finish: false,
549		width: ((cartValue * 100) / expected) + '%',
550		missingValue: expected - cartValue
551	};
552}
553
554function cgbApplyVisual(bannerEl, banner, visualState, variant) {
555	var textEl = cgbGetEl('[cartGamificationBannerText' + variant + ']', bannerEl);
556	var progressEl = cgbGetEl('[cartGamificationBannerProgress' + variant + ']', bannerEl);
557	var contentEl = cgbGetEl('.cart-gamification-banner__text', bannerEl);
558
559	if (textEl) textEl.innerHTML = visualState.text;
560	if (progressEl) progressEl.style.width = visualState.width;
561
562	var resultEnableEl = cgbGetEl('[cartGamification' + variant + 'ResultEnable]', bannerEl);
563	if (resultEnableEl && resultEnableEl.value === 'true') {
564		visualState.finish = true;
565	}
566
567	if (progressEl) {
568		cgbPaintProgressBar(progressEl, banner, visualState.finish);
569	}
570
571	if (contentEl) {
572		var color = visualState.finish && banner.corDaFonteResultado
573			? banner.corDaFonteResultado
574			: banner.corDaFonte;
575		if (color) {
576			contentEl.style.color = color;
577			if (banner.linkResultado) {
578				var linkEl = cgbGetEl('a', contentEl);
579				if (linkEl) linkEl.style.color = color;
580			}
581		}
582	}
583}
584
585function cgbPaintProgressBar(progressEl, banner, finish) {
586	var hasFinishStyle = finish &&
587		((banner.imagemDoResultado && banner.imagemDoResultado !== '') ||
588		 (banner.corDoResultado && banner.corDoResultado !== ''));
589
590	if (hasFinishStyle) {
591		if (banner.imagemDoResultado) {
592			progressEl.style.backgroundImage = "url('" + banner.imagemDoResultado + "')";
593		} else {
594			progressEl.style.backgroundColor = banner.corDoResultado;
595		}
596		if (banner.tamanhoBarraProgressoResultado) {
597			progressEl.style.height = banner.tamanhoBarraProgressoResultado;
598		}
599		return;
600	}
601
602	if (banner.imagemDoProgresso) {
603		progressEl.style.backgroundImage = "url('" + banner.imagemDoProgresso + "')";
604	} else if (banner.corDoProgresso) {
605		progressEl.style.backgroundColor = banner.corDoProgresso;
606	}
607	if (banner.tamanhoBarraProgresso) {
608		progressEl.style.height = banner.tamanhoBarraProgresso;
609	}
610	if (banner.corDaFonte) {
611		progressEl.style.color = banner.corDaFonte;
612	}
613}
614
615cartGamificationBanner.calculateBannerProgressBarOnCheckout = function (element) {
616	var variant = element || '';
617
618	if (typeof cart === 'undefined') {
619		cartGamificationBanner.calculateBannerProgressBar(variant);
620		return;
621	}
622
623	cgbSetText('[cartGamification' + variant + 'PrecoDe]', cart.subtotalDe);
624	cgbSetText('[cartGamification' + variant + 'PrecoPor]', cart.subtotal);
625	cgbSetText('[cartGamification' + variant + 'PrecoPorOriginal]', cart.subtotalPorOriginal);
626	cgbSetText('[cartGamification' + variant + 'PrecoPorTabelaDePrecoAutomatica]', cart.subtotalPorTabelaDePrecoAutomatica);
627	cgbSetText('[cartGamification' + variant + 'ClienteBonificadoTabelaDePrecoAutomatica]', cart.customerEnableForAutomaticPricingTable);
628	cgbSetText('[cartGamification' + variant + 'Itens]', cart.totalItemsQuantity);
629	cgbSetText('[cartGamification' + variant + 'PrecoComRegraPagamento]', cart.subtotalCalculado);
630
631	cartGamificationBanner.calculateBannerProgressBar(variant);
632};
633
634function cgbDetectActiveVariants() {
635	var activeVariants = [];
636	for (var i = 0; i < CGB_VARIANTS.length; i++) {
637		if (cgbGetEl('[cartGamificationBannerContainer' + CGB_VARIANTS[i] + ']')) {
638			activeVariants.push(CGB_VARIANTS[i]);
639		}
640	}
641	return activeVariants;
642}
643
644// Flag evita loop: o recalc escreve nos spans observados; libera em microtask
645// pra ignorar os MutationRecords gerados pelas próprias escritas.
646var cgbRecalcInProgress = false;
647
648function cgbRecalcAllVariants() {
649	if (cgbRecalcInProgress) return;
650	cgbRecalcInProgress = true;
651
652	try {
653		var activeVariants = cgbDetectActiveVariants();
654		for (var i = 0; i < activeVariants.length; i++) {
655			cartGamificationBanner.calculateBannerProgressBarOnCheckout(activeVariants[i]);
656		}
657	} finally {
658		Promise.resolve().then(function () {
659			cgbRecalcInProgress = false;
660		});
661	}
662}
663
664function cgbObserveBannerVisibility() {
665	if (typeof IntersectionObserver !== 'function') return;
666
667	var containers = [];
668	for (var i = 0; i < CGB_VARIANTS.length; i++) {
669		var container = cgbGetEl('[cartGamificationBannerContainer' + CGB_VARIANTS[i] + ']');
670		if (container) containers.push(container);
671	}
672	if (!containers.length) return;
673
674	var observer = new IntersectionObserver(function (entries) {
675		for (var i = 0; i < entries.length; i++) {
676			if (!entries[i].isIntersecting) continue;
677			observer.unobserve(entries[i].target);
678			cgbRecalcAllVariants();
679		}
680	}, { threshold: 0.01 });
681
682	for (var j = 0; j < containers.length; j++) {
683		observer.observe(containers[j]);
684	}
685}
686
687/**
688 * Observa mudanças no texto dos `<span>` ocultos alimentados pelo cart.js
689 * (subtotal, contagem de itens, etc.). Sempre que o backend atualiza esses
690 * valores — após add/remove de item ou após troca da forma de pagamento — o
691 * MutationObserver dispara o recálculo do banner.
692 *
693 * Em vez de escutar um evento custom (que dependeria de biblioteca externa),
694 * observamos o resultado dele no DOM. Este é o sinal natural e independente.
695 */
696function cgbObserveCartSpans() {
697	if (typeof MutationObserver !== 'function') return;
698
699	var watchedSpans = [];
700	var watchedAttrs = [
701		'PrecoPor',
702		'PrecoDe',
703		'PrecoPorOriginal',
704		'PrecoPorTabelaDePrecoAutomatica',
705		'PrecoComRegraPagamento',
706		'Itens',
707	];
708
709	for (var v = 0; v < CGB_VARIANTS.length; v++) {
710		for (var a = 0; a < watchedAttrs.length; a++) {
711			var spanEl = cgbGetEl('[cartGamification' + CGB_VARIANTS[v] + watchedAttrs[a] + ']');
712			if (spanEl) watchedSpans.push(spanEl);
713		}
714	}
715	if (!watchedSpans.length) return;
716
717	var observer = new MutationObserver(cgbRecalcAllVariants);
718	for (var i = 0; i < watchedSpans.length; i++) {
719		observer.observe(watchedSpans[i], { childList: true, characterData: true, subtree: true });
720	}
721}
722
723function cgbInit() {
724	for (var i = 0; i < CGB_VARIANTS.length; i++) {
725		var variant = CGB_VARIANTS[i];
726		if (cgbGetEl('[cartGamificationBannerItems' + variant + ']')) {
727			cartGamificationBanner.generateBanners(variant);
728		}
729	}
730
731	document.addEventListener('change', function (event) {
732		var target = event.target;
733		if (!target) return;
734		if (target.id === 'cart-items' || (target.closest && target.closest('#cart
734-items'))) {
735			cgbRecalcAllVariants();
736		}
737	});
738
739	cgbObserveBannerVisibility();
740	cgbObserveCartSpans();
741
742	cgbRecalcAllVariants();
743	setTimeout(function () {
744		if (typeof cart !== 'undefined' && cart.subtotalValue > 0) {
745			cgbRecalcAllVariants();
746		}
747	}, 600);
748}
749
750if (document.readyState === 'loading') {
751	document.addEventListener('DOMContentLoaded', cgbInit);
752} else {
753	cgbInit();
754}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.