msos/main.js

964 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

document.addEventListener('DOMContentLoaded', function() {
/**
* ===================================================================
* 1. OSNOVNA FUNKCIONALNOST MOBILNE NAVIGACIJE
* ===================================================================
*/
// === MOBILNI MENI: Preklop vidnosti menija ===
try {
const icon = document.querySelector('.mobile-menu-icon');
const root = document.documentElement;
if (!icon) {
console.error('Mobile menu icon not found.');
} else {
// Accessibility: the icon is a <div>, so expose it to keyboard and
// screen-reader users as a button (focusable, labelled, toggle state).
if (!icon.hasAttribute('role')) icon.setAttribute('role', 'button');
if (!icon.hasAttribute('tabindex')) icon.setAttribute('tabindex', '0');
if (!icon.hasAttribute('aria-label')) icon.setAttribute('aria-label', 'Open menu');
icon.setAttribute('aria-expanded', 'false');
const setMenu = (open) => {
root.classList.toggle('nav-open', open);
icon.setAttribute('aria-expanded', open ? 'true' : 'false');
icon.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
};
icon.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
setMenu(!root.classList.contains('nav-open'));
});
// Activate with Enter / Space like a native button.
icon.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
e.preventDefault();
setMenu(!root.classList.contains('nav-open'));
}
});
document.addEventListener('click', (e) => {
if (root.classList.contains('nav-open') && !e.target.closest('.mobile-nav-panel') && !e.target.closest('.mobile-menu-icon')) {
setMenu(false);
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
setMenu(false);
}
});
}
} catch (err) {
console.error('Error in Mobile Menu Toggle:', err);
}
// --- Mobilni Accordion podmeni ---
try {
const mobileNav = document.querySelector('.mobile-nav-panel .mobile-nav');
if (mobileNav) {
const menuItemsWithChildren = mobileNav.querySelectorAll('.menu-item-has-children');
menuItemsWithChildren.forEach(item => {
const link = item.querySelector('a');
if (link) {
link.addEventListener('click', (event) => {
if (event.currentTarget.parentElement.classList.contains('menu-item-has-children')) {
event.preventDefault();
}
const parentLi = event.currentTarget.parentElement;
menuItemsWithChildren.forEach(otherItem => {
if (otherItem !== parentLi && otherItem.classList.contains('open')) {
otherItem.classList.remove('open');
}
});
parentLi.classList.toggle('open');
});
}
});
}
} catch (error) {
console.error('Error in Mobile Accordion Menu:', error);
}
// Desktop: top-level dropdown parents (Who We Are, What We Do, For Students) are
// triggers, not real pages. Their href="#" caused a dead "/en/#" navigation on click.
// The submenu already opens on hover, so just cancel the navigation.
try {
document.querySelectorAll('.desktop-nav .menu-item-has-children > a').forEach((a) => {
a.addEventListener('click', (e) => { e.preventDefault(); });
});
} catch (error) {
console.error('Error disabling desktop dropdown parent links:', error);
}
/**
* ===================================================================
* 2. IZBOLJŠAVE UPORABNIŠKE IZKUŠNJE (UX/UI)
* ===================================================================
*/
// --- Funkcija za poudarjanje aktivne povezave v navigaciji ---
function highlightActiveLink() {
try {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.navigation a');
let bestMatch = null;
let longestMatch = 0;
navLinks.forEach(link => {
if (link.closest('.dropdown-menu')) return;
const linkPath = new URL(link.href, window.location.origin).pathname;
if (currentPath.startsWith(linkPath) && linkPath.length > longestMatch) {
longestMatch = linkPath.length;
bestMatch = link;
}
});
if (bestMatch) {
const parentMenuItem = bestMatch.closest('.menu-item');
if (parentMenuItem) {
parentMenuItem.classList.add('active-page');
}
}
} catch (error) {
console.error("Error highlighting active link:", error);
}
}
// --- Funkcionalnost za preklopnik jezika (Desktop Dropdown + Mobile Modal) ---
function setupLanguageSwitcher() {
try {
const path = window.location.pathname;
const pathParts = path.split('/').filter(Boolean);
const validLangs = ['en', 'si', 'mk'];
let pathWithoutLang = '/';
if (pathParts.length > 0 && validLangs.includes(pathParts[0])) {
pathWithoutLang = '/' + pathParts.slice(1).join('/');
if (path.endsWith('/') && pathWithoutLang !== '/') {
pathWithoutLang += '/';
}
} else {
pathWithoutLang = path;
}
let docLang = (document.documentElement.getAttribute('lang') || 'en').toLowerCase();
if (docLang === 'sl') docLang = 'si'; // <html lang="sl"> (correct ISO for Slovene) maps to the site's /si/ locale
document.querySelectorAll('a[data-lang]').forEach(link => {
const targetLang = link.getAttribute('data-lang');
const newUrl = `/${targetLang}${pathWithoutLang}`;
link.setAttribute('href', newUrl.replace(/\/{2,}/g, '/'));
link.classList.toggle('is-current', targetLang === docLang);
// Manually choosing a language is a deliberate preference — don't nag with the banner afterwards.
link.addEventListener('click', () => {
try { localStorage.setItem('msos-lang-banner', 'dismissed'); } catch (e) {}
});
});
const desktopSelectors = document.querySelectorAll('.header-right-wrapper .language-selector');
desktopSelectors.forEach(selector => {
const currentLang = selector.querySelector('.current-lang');
if (currentLang) {
currentLang.addEventListener('click', (event) => {
event.stopPropagation();
const isActive = selector.classList.contains('active');
document.querySelectorAll('.language-selector.active').forEach(s => s.classList.remove('active'));
if (!isActive) {
selector.classList.add('active');
}
});
}
});
window.addEventListener('click', () => {
desktopSelectors.forEach(s => s.classList.remove('active'));
});
const mobileLangButton = document.getElementById('mobile-lang-trigger');
const languageModal = document.getElementById('language-modal-overlay');
const closeModalButton = document.getElementById('language-modal-close');
if (mobileLangButton && languageModal && closeModalButton) {
mobileLangButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
languageModal.classList.add('active');
});
const closeModal = () => {
languageModal.classList.remove('active');
};
closeModalButton.addEventListener('click', closeModal);
languageModal.addEventListener('click', (e) => {
if (e.target === languageModal) closeModal();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && languageModal.classList.contains('active')) closeModal();
});
}
} catch (error) {
console.error('Error in Language Switcher setup:', error);
}
}
/**
* ===================================================================
* 3. OSTALE FUNKCIONALNOSTI (FAQ, ACTIVITIES, ITD.)
* ===================================================================
*/
// --- FAQ Accordion ---
try {
document.querySelectorAll('.faq-item h3').forEach(header => {
header.addEventListener('click', () => {
const faqItem = header.parentElement;
faqItem.classList.toggle('open');
});
});
} catch (error) {
console.error('Error in FAQ Accordion:', error);
}
// --- Activities Tabs ---
// Index-based: content lives in the HTML panels (one per language),
// so switching works regardless of the tab label language.
try {
const activityTabs = document.querySelectorAll('.activities-nav .activity-tab');
const activityPanels = document.querySelectorAll('.activities-panels .activity-panel');
if (activityTabs.length > 0 && activityPanels.length > 0) {
activityTabs.forEach(tab => {
tab.addEventListener('click', () => {
const idx = tab.getAttribute('data-panel');
activityTabs.forEach(t => {
t.classList.remove('active');
t.setAttribute('aria-selected', 'false');
});
activityPanels.forEach(p => {
p.classList.remove('active');
p.hidden = true;
});
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
const panel = document.querySelector(
`.activities-panels .activity-panel[data-panel="${idx}"]`
);
if (panel) {
panel.hidden = false;
panel.classList.add('active');
}
});
});
}
} catch (error) {
console.error('Error in Activities Tabs:', error);
}
/**
* ===================================================================
* 4. INTERAKTIVNA SEKCIJA "MISSION & VISION"
* ===================================================================
*/
try {
const missionVisionContainer = document.getElementById('mission-vision-interactive');
if (missionVisionContainer) {
const missionBox = missionVisionContainer.querySelector('.mission-box');
const visionBox = missionVisionContainer.querySelector('.vision-box');
missionBox.addEventListener('mouseenter', () => {
missionVisionContainer.classList.remove('show-vision');
});
visionBox.addEventListener('mouseenter', () => {
missionVisionContainer.classList.add('show-vision');
});
}
} catch(error) {
console.error('Error in Mission/Vision interaction setup:', error);
}
/**
* ===================================================================
* 5. SCROLLSPY IN PROGRESS BAR ZA STRANI ČLANKOV (POSODOBLJENO)
* ===================================================================
*/
try {
const articlePage = document.querySelector('.article-page');
if (articlePage) {
const articleContainer = articlePage.querySelector('.article-container');
const articleBody = articlePage.querySelector('.article-body');
const headings = articleBody ? Array.from(articleBody.querySelectorAll('h2')) : [];
const articleTitleEl = articlePage.querySelector('.article-header h1');
const articleTitle = articleTitleEl ? articleTitleEl.textContent.trim()
: (document.title || '').trim();
// Label for a heading in the table of contents. An optional
// data-nav-title attribute lets an article override the full <h2>
// text with a shorter label; otherwise we use the heading text.
const navLabel = (heading) =>
(heading.getAttribute('data-nav-title') || heading.textContent).trim();
// --- 5a. Zagotovi, da ima vsak naslov enoličen id (za sidra) ---
// Transliterate then slugify so anchors are readable in every language:
// Slovenian diacritics (č/š/ž/…) and Macedonian Cyrillic map to ASCII.
// Anything still unmapped is dropped and falls back to section-N below.
const TRANSLIT = {
// Slovenian / Latin diacritics
'č':'c','ć':'c','š':'s','ž':'z','đ':'dj',
'á':'a','à':'a','â':'a','ä':'a','ã':'a','é':'e','è':'e','ê':'e','ë':'e',
'í':'i','ì':'i','î':'i','ï':'i','ó':'o','ò':'o','ô':'o','ö':'o','õ':'o',
'ú':'u','ù':'u','û':'u','ü':'u','ñ':'n','ç':'c','ß':'ss',
// Macedonian Cyrillic
'а':'a','б':'b','в':'v','г':'g','д':'d','ѓ':'gj','е':'e','ж':'zh','з':'z',
'ѕ':'dz','и':'i','ј':'j','к':'k','л':'l','љ':'lj','м':'m','н':'n','њ':'nj',
'о':'o','п':'p','р':'r','с':'s','т':'t','ќ':'kj','у':'u','ф':'f','х':'h',
'ц':'c','ч':'ch','џ':'dj','ш':'sh'
};
const usedIds = new Set();
const transliterate = (text) =>
text.replace(/[^\x00-\x7F]/g, (ch) => (ch in TRANSLIT ? TRANSLIT[ch] : ''));
const slugify = (text) => transliterate(text.toLowerCase())
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
headings.forEach((heading, i) => {
let id = heading.id || slugify(heading.textContent);
if (id.length < 2) id = 'section-' + (i + 1);
let unique = id, n = 2;
while (usedIds.has(unique)) { unique = id + '-' + (n++); }
usedIds.add(unique);
heading.id = unique;
});
// --- 5b. Zagotovi postavitveni ovoj in stransko vrstico (Desktop) ---
// Older article pages (SI/MK and some EN) ship without the TOC
// scaffolding, so we build it here from the page's own headings.
let layoutContainer = articlePage.querySelector('.article-layout-container');
if (!layoutContainer && articleContainer) {
layoutContainer = document.createElement('div');
layoutContainer.className = 'article-layout-container';
articleContainer.parentNode.insertBefore(layoutContainer, articleContainer);
layoutContainer.appendChild(articleContainer);
}
let sidebar = articlePage.querySelector('.article-sidebar');
if (!sidebar && layoutContainer && headings.length > 0) {
sidebar = document.createElement('aside');
sidebar.className = 'article-sidebar';
sidebar.innerHTML = '<nav class="scrollspy-nav"><ul></ul></nav>';
layoutContainer.insertBefore(sidebar, layoutContainer.firstChild);
}
const scrollspyNav = sidebar ? sidebar.querySelector('.scrollspy-nav') : null;
const desktopUl = scrollspyNav ? scrollspyNav.querySelector('ul') : null;
// --- 5c. Napolni namizni seznam, če je prazen (ohrani ročno napisanega) ---
if (desktopUl && desktopUl.children.length === 0 && headings.length > 0) {
headings.forEach(heading => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = '#' + heading.id;
a.textContent = navLabel(heading);
li.appendChild(a);
desktopUl.appendChild(li);
});
}
// --- 5d. Naslov članka nad kazalom (Desktop) ---
if (sidebar && !sidebar.querySelector('.article-sidebar-title') && articleTitle) {
const titleEl = document.createElement('p');
titleEl.className = 'article-sidebar-title';
titleEl.textContent = articleTitle;
sidebar.insertBefore(titleEl, sidebar.firstChild);
}
// --- 5e. Zagotovi lepljivo mobilno glavo z naslovom + progress bar ---
let mobileHeader = articlePage.querySelector('.mobile-article-header');
if (!mobileHeader && headings.length > 0) {
mobileHeader = document.createElement('div');
mobileHeader.className = 'mobile-article-header';
mobileHeader.innerHTML =
'<span id="mobile-article-title"></span>' +
'<i class="fas fa-chevron-down"></i>' +
'<div class="progress-bar-container"><div class="progress-bar"></div></div>' +
'<div class="mobile-scrollspy-dropdown"><ul id="mobile-scrollspy-links"></ul></div>';
articlePage.insertBefore(mobileHeader, articlePage.firstChild);
}
const mobileTitle = document.getElementById('mobile-article-title');
if (mobileTitle && !mobileTitle.textContent.trim()) {
mobileTitle.textContent = articleTitle; // known at load; scrollspy refines it
}
const mobileDropdown = articlePage.querySelector('.mobile-scrollspy-dropdown');
const mobileLinksContainer = document.getElementById('mobile-scrollspy-links');
const progressBar = articlePage.querySelector('.progress-bar');
const desktopNavLinks = desktopUl ? Array.from(desktopUl.querySelectorAll('a')) : [];
// --- 5f. Napolni mobilni spustni seznam ---
if (headings.length > 0 && mobileLinksContainer && mobileLinksContainer.children.length === 0) {
headings.forEach(heading => {
const listItem = document.createElement('li');
const link = document.createElement('a');
link.href = '#' + heading.id;
link.textContent = navLabel(heading);
listItem.appendChild(link);
mobileLinksContainer.appendChild(listItem);
});
}
// --- 5g. Odpiranje/zapiranje mobilnega spustnega seznama ---
if (mobileHeader && mobileDropdown) {
mobileHeader.addEventListener('click', (e) => {
if (!e.target.closest('a')) {
mobileHeader.classList.toggle('open');
mobileDropdown.classList.toggle('open');
}
});
mobileDropdown.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileHeader.classList.remove('open');
mobileDropdown.classList.remove('open');
});
});
}
// --- 5h. Progress Bar ---
function updateProgressBar() {
if (!progressBar) return;
const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollTop = window.scrollY;
progressBar.style.width = scrollableHeight > 0
? `${(scrollTop / scrollableHeight) * 100}%`
: '0%';
}
// --- 5i. Scrollspy (posodobi navigacijo, naslov in URL sidro) ---
if (headings.length > 0) {
let activeId = null;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const id = entry.target.id;
if (id === activeId) return;
activeId = id;
// Namizna navigacija
const desktopNavLink = desktopNavLinks.find(link => link.getAttribute('href') === `#${id}`);
if (desktopNavLink) {
desktopNavLinks.forEach(link => link.classList.remove('active'));
desktopNavLink.classList.add('active');
}
// Mobilni naslov + aktivna povezava
if (mobileTitle) mobileTitle.textContent = navLabel(entry.target);
if (mobileLinksContainer) {
const mobileNavLink = mobileLinksContainer.querySelector(`a[href="#${id}"]`);
if (mobileNavLink) {
mobileLinksContainer.querySelectorAll('a').forEach(link => link.classList.remove('active'));
mobileNavLink.classList.add('active');
}
}
// Posodobi URL sidro brez skoka in brez polnjenja zgodovine
if (window.history && history.replaceState) {
history.replaceState(null, '', '#' + id);
}
});
}, {
rootMargin: "-100px 0px -55% 0px" // Sproži, ko je naslov v zgornjem delu zaslona
});
headings.forEach(heading => observer.observe(heading));
}
window.addEventListener('scroll', updateProgressBar);
updateProgressBar();
}
} catch (error) {
console.error('Error in Article Scrollspy/Progress Bar setup:', error);
}
/**
* ===================================================================
* 6. FILTER KATEGORIJ ZA SEZNAM PROJEKTOV / OBJAV
* ===================================================================
*
* Deluje na katerikoli strani s seznamom (projekti, novice, ...).
* Popolnoma jezikovno neodvisen: gumbi nosijo stabilni ključ
* (data-filter / data-subfilter), kartice pa data-category /
* data-subcategory. Oznake so lahko v katerem koli jeziku.
*/
try {
const grid = document.querySelector('.posts-grid');
if (grid) {
const filterSection = document.querySelector('.filter-section');
const pagination = document.querySelector('.pagination');
const cards = Array.from(grid.querySelectorAll('.post-card'));
const emptyMsg = document.querySelector('.filter-empty');
// Koliko kartic na stran. Ko bo objav več kot toliko, se
// paginacija samodejno vklopi; do takrat prikaže eno stran.
const PAGE_SIZE = 9;
let activeCat = 'all';
let activeSub = 'all';
let currentPage = 1;
let matching = cards.slice();
// --- Paginacija ---
const pageNumbers = pagination ? pagination.querySelector('.pagination-numbers') : null;
const arrows = pagination ? pagination.querySelectorAll('.pagination-arrow') : [];
const prevArrow = arrows.length ? arrows[0] : null;
const nextArrow = arrows.length > 1 ? arrows[arrows.length - 1] : null;
const totalPages = () => Math.max(1, Math.ceil(matching.length / PAGE_SIZE));
const renderPageNumbers = (total) => {
if (!pageNumbers) return;
pageNumbers.innerHTML = '';
const addNum = (n) => {
const a = document.createElement('a');
a.href = '#';
a.className = 'page-number' + (n === currentPage ? ' active' : '');
a.textContent = n;
a.addEventListener('click', (e) => { e.preventDefault(); goToPage(n); });
pageNumbers.appendChild(a);
};
const addDots = () => {
const s = document.createElement('span');
s.className = 'page-dots';
s.textContent = '...';
pageNumbers.appendChild(s);
};
// Do 7 strani pokažemo vse; sicer okno okoli trenutne strani.
const seq = [];
if (total <= 7) {
for (let i = 1; i <= total; i++) seq.push(i);
} else {
seq.push(1);
const start = Math.max(2, currentPage - 1);
const end = Math.min(total - 1, currentPage + 1);
if (start > 2) seq.push('...');
for (let i = start; i <= end; i++) seq.push(i);
if (end < total - 1) seq.push('...');
seq.push(total);
}
seq.forEach(p => (p === '...' ? addDots() : addNum(p)));
};
const render = () => {
const total = totalPages();
if (currentPage > total) currentPage = total;
if (currentPage < 1) currentPage = 1;
cards.forEach(c => { c.hidden = true; });
const start = (currentPage - 1) * PAGE_SIZE;
matching.slice(start, start + PAGE_SIZE).forEach(c => { c.hidden = false; });
if (emptyMsg) emptyMsg.hidden = matching.length !== 0;
if (pagination) {
// Skrij paginacijo, ko ni zadetkov.
pagination.hidden = matching.length === 0;
renderPageNumbers(total);
if (prevArrow) prevArrow.classList.toggle('disabled', currentPage <= 1);
if (nextArrow) nextArrow.classList.toggle('disabled', currentPage >= total);
}
};
function goToPage(n) {
const total = totalPages();
currentPage = Math.min(Math.max(1, n), total);
render();
// Po menjavi strani se pomaknemo na vrh seznama.
const anchor = filterSection || grid;
const y = anchor.getBoundingClientRect().top + window.scrollY - 100;
window.scrollTo({ top: y, behavior: 'smooth' });
}
if (prevArrow) {
prevArrow.addEventListener('click', (e) => {
e.preventDefault();
if (currentPage > 1) goToPage(currentPage - 1);
});
}
if (nextArrow) {
nextArrow.addEventListener('click', (e) => {
e.preventDefault();
if (currentPage < totalPages()) goToPage(currentPage + 1);
});
}
// --- Filter (če je na strani) ---
const computeMatching = () => {
matching = cards.filter(card => {
const cat = (card.getAttribute('data-category') || '').trim();
const sub = (card.getAttribute('data-subcategory') || '').trim();
let show = activeCat === 'all' || cat === activeCat;
if (show && activeCat !== 'all' && activeSub !== 'all') {
show = sub === activeSub;
}
return show;
});
};
if (filterSection) {
const pills = Array.from(filterSection.querySelectorAll('.filter-pill'));
const subnavs = Array.from(filterSection.querySelectorAll('.filter-subnav'));
const showSubnavFor = (cat) => {
subnavs.forEach(sn => {
const match = sn.getAttribute('data-parent') === cat;
sn.hidden = !match;
if (match) {
sn.querySelectorAll('.filter-chip').forEach(chip =>
chip.classList.toggle('active',
(chip.getAttribute('data-subfilter') || 'all') === 'all'));
}
});
};
pills.forEach(pill => {
pill.addEventListener('click', () => {
activeCat = pill.getAttribute('data-filter') || 'all';
activeSub = 'all';
currentPage = 1;
pills.forEach(p => {
const on = p === pill;
p.classList.toggle('active', on);
p.setAttribute('aria-pressed', on ? 'true' : 'false');
});
showSubnavFor(activeCat);
computeMatching();
render();
});
});
subnavs.forEach(sn => {
sn.querySelectorAll('.filter-chip').forEach(chip => {
chip.addEventListener('click', () => {
activeSub = chip.getAttribute('data-subfilter') || 'all';
currentPage = 1;
sn.querySelectorAll('.filter-chip').forEach(c =>
c.classList.toggle('active', c === chip));
computeMatching();
render();
});
});
});
// Začetno stanje: "Vse" aktivno, podfiltri skriti.
showSubnavFor(activeCat);
}
computeMatching();
render();
}
} catch (error) {
console.error('Error in Project Filter/Pagination setup:', error);
}
/**
* ===================================================================
* 7. NEWSLETTER (MailerLite)
* ===================================================================
*/
function setupNewsletter() {
// --- MailerLite configuration -----------------------------------
// Fill these in from your MailerLite embedded form
// (Forms > Embedded forms > your form > Embed code). Both values
// appear in the embed's subscribe URL:
// https://assets.mailerlite.com/jsonp/<ACCOUNT>/forms/<FORM>/subscribe
const ML_ACCOUNT = '1253161';
const ML_FORM = '194374752800868123';
// ----------------------------------------------------------------
const forms = document.querySelectorAll('form.subscribe-form, form.subscribe-form-bottom');
if (!forms.length) return;
let lang = (document.documentElement.getAttribute('lang') || 'en').toLowerCase();
if (lang === 'sl') lang = 'si'; // <html lang="sl"> maps to the site's /si/ locale
if (['en', 'si', 'mk'].indexOf(lang) === -1) lang = 'en';
const T = {
invalid: { en: 'Please enter a valid email address.', si: 'Vnesite veljaven e-poštni naslov.', mk: 'Внесете важечка е-адреса.' },
sending: { en: 'Sending…', si: 'Pošiljanje…', mk: 'Се испраќа…' },
success: { en: 'Thank you! Please check your inbox to confirm your subscription.', si: 'Hvala! Za potrditev naročnine preverite svoj e-poštni predal.', mk: 'Ви благодариме! Проверете го вашето сандаче за да ја потврдите претплатата.' },
error: { en: 'Something went wrong. Please try again later.', si: 'Nekaj je šlo narobe. Poskusite znova pozneje.', mk: 'Нешто не успеа. Обидете се повторно подоцна.' },
notready: { en: 'The newsletter is being set up. Please check back soon.', si: 'Novice se pravkar nastavljajo. Preverite kmalu znova.', mk: 'Билтенот се поставува. Проверете повторно наскоро.' }
};
const t = (k) => (T[k][lang] || T[k].en);
forms.forEach((form) => {
const input = form.querySelector('input[type="email"], input[type="text"], input');
if (!input) return;
input.setAttribute('type', 'email');
input.setAttribute('name', 'fields[email]');
input.setAttribute('required', '');
input.setAttribute('autocomplete', 'email');
const status = document.createElement('p');
status.className = 'subscribe-status';
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
status.hidden = true;
form.parentNode.insertBefore(status, form.nextSibling);
const show = (msg, type) => {
status.textContent = msg;
status.className = 'subscribe-status is-' + type;
status.hidden = false;
};
form.addEventListener('submit', (e) => {
e.preventDefault();
const email = (input.value || '').trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
show(t('invalid'), 'error');
input.focus();
return;
}
if (!ML_ACCOUNT || !ML_FORM) { show(t('notready'), 'error'); return; }
const button = form.querySelector('button[type="submit"], button');
if (button) button.disabled = true;
show(t('sending'), 'info');
const url = 'https://assets.mailerlite.com/jsonp/' + ML_ACCOUNT + '/forms/' + ML_FORM + '/subscribe';
const body = new URLSearchParams();
body.append('fields[email]', email);
body.append('ml-submit', '1');
body.append('anticsrf', 'true');
fetch(url, { method: 'POST', body: body, mode: 'no-cors' })
.then(() => {
// Double opt-in: MailerLite emails a confirmation link, so we
// optimistically confirm here (the opt-in email is the real check).
form.reset();
show(t('success'), 'success');
})
.catch(() => show(t('error'), 'error'))
.finally(() => { if (button) button.disabled = false; });
});
});
}
/**
* ===================================================================
* LANGUAGE SUGGESTION BANNER (browser-language based, dismissible)
* ===================================================================
*/
function setupLanguageSuggestion() {
try {
var STORE = 'msos-lang-banner';
if (localStorage.getItem(STORE) === 'dismissed') return;
var validLangs = ['en', 'si', 'mk'];
// Current page language (<html lang>; site uses /si/ for Slovene "sl").
var docLang = (document.documentElement.getAttribute('lang') || 'en').toLowerCase();
if (docLang === 'sl') docLang = 'si';
// Detect the visitor's preferred language from the browser.
var navLangs = navigator.languages || [navigator.language || ''];
var detected = null;
for (var i = 0; i < navLangs.length; i++) {
var l = (navLangs[i] || '').toLowerCase();
if (l.indexOf('mk') === 0) { detected = 'mk'; break; }
if (l.indexOf('sl') === 0) { detected = 'si'; break; }
if (l.indexOf('en') === 0) { detected = 'en'; break; }
}
if (!detected || detected === docLang) return; // unknown or already correct
// Build the equivalent URL in the detected language (same logic as the switcher).
var path = window.location.pathname;
var parts = path.split('/').filter(Boolean);
var pathWithoutLang = path;
if (parts.length > 0 && validLangs.indexOf(parts[0]) !== -1) {
pathWithoutLang = '/' + parts.slice(1).join('/');
if (path.endsWith('/') && pathWithoutLang !== '/') pathWithoutLang += '/';
}
var targetUrl = ('/' + detected + pathWithoutLang).replace(/\/{2,}/g, '/');
var MSG = {
en: { text: 'This page is also available in English.', btn: 'View in English', flag: 'gb', aria: 'Language suggestion' },
mk: { text: 'Оваа страница е достапна и на македонски.', btn: 'Прикажи на македонски', flag: 'mk', aria: 'Предлог за јазик' },
si: { text: 'Ta stran je na voljo tudi v slovenščini.', btn: 'Prikaži v slovenščini', flag: 'si', aria: 'Predlog jezika' }
};
var m = MSG[detected];
function showBanner() {
if (document.querySelector('.lang-suggest')) return;
var bar = document.createElement('div');
bar.className = 'lang-suggest';
bar.setAttribute('role', 'region');
bar.setAttribute('aria-label', m.aria);
bar.innerHTML =
'<span class="lang-flag lang-flag-' + m.flag + '" aria-hidden="true"></span>' +
'<p class="lang-suggest-text">' + m.text + '</p>' +
'<a class="lang-suggest-btn" href="' + targetUrl + '">' + m.btn + '</a>' +
'<button type="button" class="lang-suggest-close" aria-label="Dismiss">&times;</button>';
document.body.appendChild(bar);
requestAnimationFrame(function () { bar.classList.add('is-visible'); });
function remember() { try { localStorage.setItem(STORE, 'dismissed'); } catch (e) {} }
bar.querySelector('.lang-suggest-btn').addEventListener('click', remember);
bar.querySelector('.lang-suggest-close').addEventListener('click', function () {
remember();
bar.classList.remove('is-visible');
setTimeout(function () { bar.remove(); }, 300);
});
}
// Don't collide with the cookie-consent card (also a bottom-centred fixed card).
// If the visitor hasn't answered consent yet, wait until they do, then show.
var consentDecided;
try {
var c = localStorage.getItem('msos-consent');
consentDecided = (c === 'granted' || c === 'denied');
} catch (e) { consentDecided = true; }
if (consentDecided && !document.querySelector('.msos-cc')) {
showBanner();
} else if ('MutationObserver' in window) {
var obs = new MutationObserver(function () {
if (!document.querySelector('.msos-cc')) { obs.disconnect(); showBanner(); }
});
obs.observe(document.body, { childList: true });
} else {
// Fallback: poll until the consent card is gone.
var tries = 0;
var iv = setInterval(function () {
if (!document.querySelector('.msos-cc') || ++tries > 120) { clearInterval(iv); showBanner(); }
}, 500);
}
} catch (error) {
console.error('Error in language suggestion banner:', error);
}
}
/**
* ===================================================================
* REUSABLE LIGHTBOX (for .event-gallery-grid.js-lightbox blocks)
* ===================================================================
*/
function setupLightbox() {
var grids = document.querySelectorAll('.js-lightbox');
if (!grids.length) return;
var lb = document.createElement('div');
lb.className = 'lightbox';
lb.setAttribute('role', 'dialog');
lb.setAttribute('aria-label', 'Photo viewer');
lb.setAttribute('aria-hidden', 'true');
lb.innerHTML =
'<button type="button" class="lightbox-close" aria-label="Close">&times;</button>' +
'<button type="button" class="lightbox-prev" aria-label="Previous">&#10094;</button>' +
'<img class="lightbox-img" src="" alt="">' +
'<button type="button" class="lightbox-next" aria-label="Next">&#10095;</button>' +
'<div class="lightbox-caption"></div>';
document.body.appendChild(lb);
var img = lb.querySelector('.lightbox-img');
var cap = lb.querySelector('.lightbox-caption');
var group = [];
var idx = 0;
function render() {
var it = group[idx];
img.setAttribute('src', it.full);
img.setAttribute('alt', it.caption || '');
cap.textContent = (it.caption || '') + (group.length > 1 ? ' (' + (idx + 1) + '/' + group.length + ')' : '');
}
function open(grid, start) {
group = Array.prototype.map.call(grid.querySelectorAll('.eg-thumb'), function (b) {
return { full: b.getAttribute('data-full'), caption: b.getAttribute('data-caption') || '' };
});
idx = start;
render();
lb.classList.add('is-open');
lb.setAttribute('aria-hidden', 'false');
document.documentElement.style.overflow = 'hidden';
}
function close() {
lb.classList.remove('is-open');
lb.setAttribute('aria-hidden', 'true');
document.documentElement.style.overflow = '';
img.setAttribute('src', '');
}
function step(d) { idx = (idx + d + group.length) % group.length; render(); }
grids.forEach(function (grid) {
grid.querySelectorAll('.eg-thumb').forEach(function (b, i) {
b.addEventListener('click', function () { open(grid, i); });
});
});
lb.querySelector('.lightbox-close').addEventListener('click', close);
lb.querySelector('.lightbox-prev').addEventListener('click', function () { step(-1); });
lb.querySelector('.lightbox-next').addEventListener('click', function () { step(1); });
lb.addEventListener('click', function (e) { if (e.target === lb) close(); });
document.addEventListener('keydown', function (e) {
if (!lb.classList.contains('is-open')) return;
if (e.key === 'Escape') close();
else if (e.key === 'ArrowLeft') step(-1);
else if (e.key === 'ArrowRight') step(1);
});
// Swipe on touch devices
var sx = 0;
lb.addEventListener('touchstart', function (e) { sx = e.changedTouches[0].clientX; }, { passive: true });
lb.addEventListener('touchend', function (e) {
var dx = e.changedTouches[0].clientX - sx;
if (Math.abs(dx) > 40) step(dx < 0 ? 1 : -1);
}, { passive: true });
}
/**
* ===================================================================
* HUB USE-CASE CAROUSEL (auto-scrolling)
* ===================================================================
*/
function setupHubCarousel() {
const trackEl = document.querySelector('.hub-track');
if (!trackEl) return;
const group = trackEl.querySelector('.hub-group');
if (!group) return;
// Duplicate the group so translateX(-50%) loops back seamlessly (waterslide flow).
// Guard against double-cloning if the script somehow runs twice.
if (trackEl.querySelectorAll('.hub-group').length < 2) {
const clone = group.cloneNode(true);
clone.setAttribute('aria-hidden', 'true');
trackEl.appendChild(clone);
}
}
/**
* ===================================================================
* 8. ZAGON FUNKCIJ PO NALOŽITVI STRANI
* ===================================================================
*/
highlightActiveLink();
setupLanguageSwitcher();
setupLanguageSuggestion();
setupNewsletter();
setupHubCarousel();
setupLightbox();
});