/* =====================================================================
Car Search Tree — standalone widget
Paste into the store's Custom JS (Theme > Custom JS), or host it and
load it with */
if (window.voltStandaloneCarSearchConfig && typeof window.voltStandaloneCarSearchConfig === 'object') {
Object.keys(window.voltStandaloneCarSearchConfig).forEach(function (key) {
CONFIG[key] = window.voltStandaloneCarSearchConfig[key];
});
}
/* Class-name prefix — single source of truth, mirrors the CSS file. */
var NS = 'volt-standalone-';
var HIDDEN = NS + 'hidden';
var ACTIVE = NS + 'is-active';
/* =================================================================
Helpers
================================================================= */
function esc(value) {
return String(value === null || value === undefined ? '' : value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function kids(node) {
return node && Array.isArray(node.sub_categories) ? node.sub_categories : [];
}
function findById(list, id) {
if (!Array.isArray(list)) return null;
for (var i = 0; i < list.length; i++) {
if (String(list[i].id) === String(id)) return list[i];
}
return null;
}
/* =================================================================
Widget
================================================================= */
var state = { brand: null, model: null, year: null };
var el = {};
var root = null;
var isDropdown = CONFIG.use_dropdown_layout === true;
var ROW_OF = { brand: 'brands', model: 'models', year: 'years' };
function titleStyle() {
var size = CONFIG.title_size > 0 ? CONFIG.title_size : 36;
var out = 'font-size: clamp(' + Math.round(size * 0.6) + 'px, 6vw, ' + size + 'px);';
if (CONFIG.title_color_type === 'primary') out += ' color: var(--' + NS + 'primary);';
else if (CONFIG.title_color_type === 'custom') out += ' color: ' + CONFIG.title_color + ';';
if (CONFIG.title_font) out += ' font-family: ' + CONFIG.title_font + ';';
return out;
}
function subtitleStyle() {
var size = CONFIG.sub_title_size > 0 ? CONFIG.sub_title_size : 18;
return 'font-size: clamp(' + Math.round(size * 0.8) + 'px, 2.5vw, ' + size + 'px);';
}
function selectBlock(level, label) {
var id = NS + level + '-select';
return '' +
'
' +
'' +
'' +
'
';
}
function template() {
return '' +
'' +
'' +
'
' +
'
' + esc(CONFIG.empty_title) + '' +
'
' + esc(CONFIG.empty_description) + '
' +
'
' +
'
' +
'
' +
selectBlock('brand', CONFIG.brand_label) +
selectBlock('model', CONFIG.model_label) +
selectBlock('year', CONFIG.year_label) +
'
' +
(CONFIG.show_search_button ?
'
' +
'' +
'
' : '') +
'
';
}
function show(node) { if (node) node.classList.remove(HIDDEN); }
function hide(node) { if (node) node.classList.add(HIDDEN); }
/* ---- Options ---------------------------------------------------- */
function optionTemplate(item, level) {
var name = item && item.name ? item.name : '';
var title = name;
var subtitle = '';
if (level === 'year' && name.indexOf('(') !== -1) {
var idx = name.indexOf('(');
title = name.substring(0, idx).trim();
subtitle = name.substring(idx).trim();
}
var isYear = level === 'year';
var thumbClass = NS + 'thumb';
var thumb;
if (isYear) {
return '' +
'';
} else if (item && item.image) {
thumb = '
';
} else {
thumbClass += ' ' + NS + 'thumb--fallback';
thumb = '' + esc(name.charAt(0) || '?') + '';
}
var hideLabel = level === 'brand' && CONFIG.hide_brand_labels;
return '' +
'';
}
function setupMobileScrollbar(container) {
if (!container || isDropdown) return;
var wrapper = container.parentNode;
if (!wrapper) return;
wrapper.classList.add(NS + 'mobile-scroll-wrap');
var bar = wrapper.querySelector('.' + NS + 'mobile-scrollbar');
if (!bar) {
bar = document.createElement('div');
bar.className = NS + 'mobile-scrollbar';
bar.innerHTML = '';
wrapper.appendChild(bar);
container.addEventListener('scroll', function () {
updateMobileScrollbar(container, bar);
}, { passive: true });
window.addEventListener('resize', function () {
updateMobileScrollbar(container, bar);
});
}
updateMobileScrollbar(container, bar);
}
function updateMobileScrollbar(container, bar) {
if (!container || !bar) return;
var thumb = bar.querySelector('.' + NS + 'mobile-scroll-thumb');
if (!thumb) return;
var viewport = container.clientWidth;
var content = container.scrollWidth;
var maxScroll = Math.max(0, content - viewport);
if (maxScroll <= 2) {
bar.style.display = 'none';
return;
}
bar.style.display = 'block';
var trackWidth = bar.clientWidth || viewport;
var minThumb = 34;
var thumbWidth = Math.max(minThumb, Math.round(trackWidth * viewport / content));
thumbWidth = Math.min(trackWidth, thumbWidth);
var distanceFromRight;
if (container.scrollLeft < 0) {
distanceFromRight = Math.abs(container.scrollLeft);
} else {
distanceFromRight = maxScroll - container.scrollLeft;
}
distanceFromRight = Math.max(0, Math.min(maxScroll, distanceFromRight));
var travel = Math.max(0, trackWidth - thumbWidth);
var position = maxScroll ? travel - ((distanceFromRight / maxScroll) * travel) : travel;
thumb.style.width = thumbWidth + 'px';
thumb.style.transform = 'translateX(' + position + 'px)';
}
function renderOptions(level, items) {
if (isDropdown) return renderSelectOptions(level, items);
var container = el.rows[ROW_OF[level]];
if (!container) return;
var list = Array.isArray(items) ? items : [];
if (!list.length) {
container.innerHTML = '';
hide(el.wrappers[ROW_OF[level]]);
return;
}
show(el.wrappers[ROW_OF[level]]);
container.classList.add('leaving');
setTimeout(function () {
container.innerHTML = list.map(function (item) {
return optionTemplate(item, level);
}).join('');
container.querySelectorAll('.' + NS + 'option').forEach(function (button) {
button.addEventListener('click', function () {
select(level, button.dataset.id);
});
});
container.classList.remove('leaving');
container.classList.add('entering');
setupMobileScrollbar(container);
requestAnimationFrame(function () {
container.classList.remove('entering');
});
}, 180);
}
function placeholderFor(level) {
return { brand: CONFIG.brand_label, model: CONFIG.model_label, year: CONFIG.year_label }[level] || '';
}
function renderSelectOptions(level, items) {
var select = el.selects[level];
if (!select) return;
var list = Array.isArray(items) ? items : [];
select.innerHTML = '' +
list.map(function (item) {
return '';
}).join('');
setSelectDisabled(level, !list.length);
select.value = '';
}
function clearSelect(level) {
var select = el.selects[level];
if (!select) return;
select.innerHTML = '';
setSelectDisabled(level, level !== 'brand');
select.value = '';
}
function setSelectDisabled(level, disabled) {
if (el.selects[level]) el.selects[level].disabled = !!disabled;
var wrapper = el.selectWrappers[level];
if (wrapper) wrapper.classList.toggle(NS + 'select--disabled', !!disabled);
}
function clearLevel(level) {
if (isDropdown) return clearSelect(level);
var container = el.rows[ROW_OF[level]];
if (container) container.innerHTML = '';
hide(el.wrappers[ROW_OF[level]]);
}
function setActive(level, id) {
if (isDropdown) return;
var container = el.rows[ROW_OF[level]];
if (!container) return;
container.querySelectorAll('.' + NS + 'option').forEach(function (button) {
button.classList.toggle(ACTIVE, String(button.dataset.id) === String(id));
});
}
/* ---- Selection ------------------------------------------------- */
function select(level, id) {
if (!id) return;
if (level === 'brand') return selectBrand(id);
if (level === 'model') return selectModel(id);
if (level === 'year') return selectYear(id);
}
function selectBrand(id) {
var brand = findById(CONFIG.categories, id);
if (!brand) return;
state.brand = brand;
state.model = null;
state.year = null;
setActive('brand', id);
renderOptions('model', kids(brand));
clearLevel('year');
updateSelectionPath();
if (kids(brand).length) {
show(el.wrappers.models);
} else if (brand.url) {
window.location.href = brand.url;
}
}
function selectModel(id) {
if (!state.brand) return;
var model = findById(kids(state.brand), id);
if (!model) return;
state.model = model;
state.year = null;
setActive('model', id);
renderOptions('year', kids(model));
updateSelectionPath();
if (!kids(model).length && model.url) {
window.location.href = model.url;
return;
}
}
function selectYear(id) {
if (!state.model) return;
var year = findById(kids(state.model), id);
if (!year) return;
state.year = year;
setActive('year', id);
updateSelectionPath();
if (year.url) {
window.location.href = year.url;
}
}
function updateSelectionPath() {
var parts = [];
if (state.brand) parts.push(state.brand.name);
if (state.model) parts.push(state.model.name);
if (state.year) parts.push(state.year.name);
el.selection.textContent = parts.length ? parts.join(' / ') : '—';
if (el.submitBtn) el.submitBtn.disabled = !state.brand;
}
/* ---- Mount ----------------------------------------------------- */
function cacheRefs() {
var q = function (sel) { return root.querySelector(sel); };
el = {
rows: {
brands: q('[data-row="brands"]'),
models: q('[data-row="models"]'),
years: q('[data-row="years"]'),
},
wrappers: {
brands: q('[data-wrapper="brands"]'),
models: q('[data-wrapper="models"]'),
years: q('[data-wrapper="years"]')
},
selects: {
brand: q('[data-select="brand"]'),
model: q('[data-select="model"]'),
year: q('[data-select="year"]')
},
selectWrappers: {
brand: q('[data-select-wrapper="brand"]'),
model: q('[data-select-wrapper="model"]'),
year: q('[data-select-wrapper="year"]')
},
empty: q('[data-empty]'),
selection: q('[data-selected-path]'),
submitBtn: q('[data-submit-btn]')
};
}
/** Apply the CONFIG colours as CSS custom properties on the root. */
function applyTheme() {
if (CONFIG.primary_color) root.style.setProperty('--' + NS + 'primary', CONFIG.primary_color);
if (CONFIG.accent_color) root.style.setProperty('--' + NS + 'accent', CONFIG.accent_color);
if (CONFIG.border_radius) root.style.setProperty('--' + NS + 'radius', CONFIG.border_radius);
// 'auto' needs no class — the CSS follows the store's own dark-mode class
if (['light', 'dark', 'system'].indexOf(CONFIG.color_scheme) > -1) {
root.classList.add(NS + 'scheme-' + CONFIG.color_scheme);
}
}
/** Warn (once) if the companion stylesheet was never added to the store. */
function verifyStylesheet() {
var padding = window.getComputedStyle(root).paddingTop;
if (padding === '0px' || padding === '') {
console.warn(
'[volt-standalone-car-search] stylesheet not detected — ' +
'add volt-standalone-car-search.css to the store\'s Custom CSS.'
);
}
}
function mount() {
if (document.querySelector('.' + NS + 'root')) return; // already mounted
var slug = (window.salla && window.salla.config && window.salla.config.get)
? window.salla.config.get('page.slug') : null;
if (Array.isArray(CONFIG.pages) && CONFIG.pages.length && CONFIG.pages.indexOf(slug) === -1) return;
var slider = document.querySelector('section.advanced-slider');
/* إذا لم نجد السلايدر لا نعرض القالب */
if (!slider) {
return;
}
var host = slider.parentNode;
root = document.createElement('section');
root.className = NS + 'root';
root.setAttribute('dir', document.documentElement.dir || 'rtl');
root.innerHTML = template();
slider.insertAdjacentElement('afterend', root);
applyTheme();
cacheRefs();
verifyStylesheet();
if (isDropdown) {
el.selects.brand && el.selects.brand.addEventListener('change', function (e) { select('brand', e.target.value); });
el.selects.model && el.selects.model.addEventListener('change', function (e) { select('model', e.target.value); });
el.selects.year && el.selects.year.addEventListener('change', function (e) { select('year', e.target.value); });
clearSelect('model');
clearSelect('year');
}
if (el.submitBtn) el.submitBtn.addEventListener('click', submitSearch);
if (!Array.isArray(CONFIG.categories) || !CONFIG.categories.length) {
show(el.empty);
return;
}
renderOptions('brand', CONFIG.categories);
if (!isDropdown) show(el.wrappers.brands);
if (!CONFIG.show_search_button) applyUrlPreselection();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mount);
} else {
mount();
}
}());