<script>
const CONFIG = {
CACHE_KEY: 'tilda_parts_cache_v1',
CACHE_TTL: 24 * 60 * 60 * 1000,
API_BASE: 'https://store.tildaapi.com/api/getproductslist/',
DEBUG: false,
TIMINGS: {
GRID_RENDER_DELAY: 500,
WAIT_PART_ELEMENTS: 10000,
WAIT_XHR: 10000,
FALLBACK_TIMEOUT: 8000,
RETRY_DELAY: 1000,
MAX_RETRIES: 3
}
};
function log(...args) {
if (CONFIG.DEBUG) console.log('[PART-TOTAL]', ...args);
}
function getRealGrids() {
return Array.from(document.querySelectorAll('[class*="-grid-cont"]'))
.filter(el => !Array.from(el.classList).some(cls => cls.includes('preloader')));
}
function waitForTStoreXHR(timeout = CONFIG.TIMINGS.WAIT_XHR) {
return new Promise((resolve) => {
if (window.tStoreXHR) {
resolve(window.tStoreXHR);
return;
}
const start = Date.now();
const interval = setInterval(() => {
if (window.tStoreXHR) {
clearInterval(interval);
resolve(window.tStoreXHR);
} else if (Date.now() - start >= timeout) {
clearInterval(interval);
resolve({});
}
}, 200);
});
}
function waitForPartElements(timeout = CONFIG.TIMINGS.WAIT_PART_ELEMENTS) {
return new Promise((resolve) => {
const start = Date.now();
const check = () => {
const els = document.querySelectorAll('[data-storepart-uid]');
if (els.length > 0) {
resolve(els);
} else if (Date.now() - start >= timeout) {
resolve([]);
} else {
setTimeout(check, 300);
}
};
check();
});
}
function flattenParts(parts, sections = {}, seenUids = new Set(), basePath = []) {
if (!Array.isArray(parts)) return { sections, seenUids };
for (const part of parts) {
const { uid, title, path, subparts } = part;
if (!uid || !title || seenUids.has(uid)) continue;
seenUids.add(uid);
const searchKeys = [String(uid)];
if (title) searchKeys.push(title);
const pathArray = Array.isArray(path) && path.length > 0
? path
: (basePath.length > 0 ? [...basePath, title] : [title]);
if (pathArray.length > 0) {
const pathKey = pathArray.join('/@/');
if (pathKey !== title) searchKeys.push(pathKey);
}
searchKeys.forEach(key => {
if (!sections[key]) sections[key] = { uid: String(uid), total: 0 };
});
if (Array.isArray(subparts) && subparts.length > 0) {
flattenParts(subparts, sections, seenUids, pathArray);
}
}
return { sections, seenUids };
}
async function extractSectionsFromStore() {
const store = await waitForTStoreXHR();
if (!Object.keys(store).length) return {};
const sections = {};
const seenUids = new Set();
Object.values(store).forEach(block => {
try {
if (!block?.response) return;
const response = JSON.parse(block.response);
if (!Array.isArray(response.parts)) return;
flattenParts(response.parts, sections, seenUids);
} catch (e) {
console.error('Error parsing store response:', e.message);
}
});
return sections;
}
function fetchPartTotal(partUid) {
return fetch(`${CONFIG.API_BASE}?storepartuid=${partUid}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
})
.then(res => res.ok ? res.json() : Promise.reject(`HTTP ${res.status}`))
.then(json => ({
partUid: String(partUid),
total: typeof json.total === 'number' ? json.total : 0
}))
.catch(err => {
console.error(`Failed to fetch total for UID ${partUid}:`, err);
return { partUid: String(partUid), total: 0 };
});
}
function findTargetElement(baseEl) {
if (Array.from(baseEl.classList).some(cls => cls.includes('parts-switch-btn-all'))) {
return baseEl;
}
const selectors = [
'[class*="parts-item-title"]',
'[class*="parts-tree-btn-title"]',
'[class*="filter__title"]'
];
for (const sel of selectors) {
const el = baseEl.querySelector(sel);
if (el) return el;
}
if (baseEl.tagName?.toLowerCase() === 'input') {
const label = baseEl.closest('label');
if (label) {
const el = label.querySelector('[class*="filter-tree-label"]');
if (el) return el;
}
}
return null;
}
function updateElements(results) {
let updatedCount = 0;
results.forEach(({ name, total, uid }) => {
const searchValues = [
uid,
name,
...(name.includes('/@/') ? [name.split('/@/').pop()] : [])
];
const uniqueValues = [...new Set(searchValues.filter(v => v))];
uniqueValues.forEach(value => {
const selector = `[data-storepart-uid="${value}"]`;
document.querySelectorAll(selector).forEach(baseEl => {
const target = findTargetElement(baseEl);
if (target && !target.hasAttribute('data-total-product')) {
target.setAttribute('data-total-product', total);
updatedCount++;
}
});
});
});
return updatedCount;
}
function getCachedData() {
try {
const raw = localStorage.getItem(CONFIG.CACHE_KEY);
if (!raw) return null;
const { ts, sections, data } = JSON.parse(raw);
if (Date.now() - ts < CONFIG.CACHE_TTL) {
return { sections, data };
}
} catch (e) {}
return null;
}
function saveToCache(sections, data) {
try {
localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify({
ts: Date.now(), sections, data
}));
} catch (e) {}
}
async function processCatalogSections(attempt = 1) {
const cached = getCachedData();
if (cached) {
const results = Object.entries(cached.data).map(([name, { total, uid }]) => ({
name, total, uid
}));
const updated = updateElements(results);
if (updated === 0 && attempt < CONFIG.TIMINGS.MAX_RETRIES) {
await new Promise(r => setTimeout(r, CONFIG.TIMINGS.RETRY_DELAY));
return processCatalogSections(attempt + 1);
}
return;
}
await waitForPartElements();
const sections = await extractSectionsFromStore();
const entries = Object.entries(sections);
if (!entries.length) {
return;
}
const results = await Promise.all(
entries.map(([name, { uid }]) =>
fetchPartTotal(uid).then(res => ({ name, total: res.total, uid }))
)
);
const cacheData = results.reduce((acc, { name, total, uid }) => {
acc[name] = { total, uid };
return acc;
}, {});
saveToCache(sections, cacheData);
const updated = updateElements(results);
if (updated === 0 && attempt < CONFIG.TIMINGS.MAX_RETRIES) {
await new Promise(r => setTimeout(r, CONFIG.TIMINGS.RETRY_DELAY));
return processCatalogSections(attempt + 1);
}
}
document.addEventListener('DOMContentLoaded', function() {
const grids = getRealGrids();
if (!grids.length) {
return;
}
let rendered = 0;
const total = grids.length;
let processed = false;
const onRendered = function(e) {
if (!e.target.matches('[class*="-grid-cont"]')) return;
if (Array.from(e.target.classList).some(cls => cls.includes('preloader'))) return;
rendered++;
if (rendered === total && !processed) {
processed = true;
setTimeout(() => {
processCatalogSections();
}, CONFIG.TIMINGS.GRID_RENDER_DELAY);
grids.forEach(grid => grid.removeEventListener('tStoreRendered', onRendered));
}
};
grids.forEach(grid => grid.addEventListener('tStoreRendered', onRendered));
setTimeout(() => {
if (!processed) {
processed = true;
setTimeout(() => processCatalogSections(), CONFIG.TIMINGS.GRID_RENDER_DELAY);
grids.forEach(grid => grid.removeEventListener('tStoreRendered', onRendered));
}
}, CONFIG.TIMINGS.FALLBACK_TIMEOUT);
});
</script>
<style>
[class*="parts-switch-btn-all"][data-total-product]:after,
[class*="parts-item-title"][data-total-product]:after,
[class*="parts-tree-btn-title"][data-total-product]:after,
[class*="filter__title"][data-total-product]:after,
[class*="filter-tree-label"][data-total-product]:after,
.parts-switch-btn-all[data-total-product]:after {
content: attr(data-total-product);
font-size: 0.7em;
opacity: 0.5;
pointer-events: none;
padding: 2px 2px;
white-space: nowrap;
position: relative;
top: -6px;
}
[class*="parts-switch-btn-all"][data-total-product] {
justify-content: flex-start;
}
.js-store-parts-switcher,
[class*="parts-tree-btn"],
[class*="filter-tree-item"],
[class*="filter__item"],
.parts-switch-btn-all {
position: relative;
}
.t-store__parts-item-title {
display: flex;
align-items: center;
gap: 0px;
}
</style>