1136 lines
52 KiB
JavaScript
1136 lines
52 KiB
JavaScript
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">×</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">×</button>' +
|
||
'<button type="button" class="lightbox-prev" aria-label="Previous">❮</button>' +
|
||
'<img class="lightbox-img" src="" alt="">' +
|
||
'<button type="button" class="lightbox-next" aria-label="Next">❯</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 + drag + position dots)
|
||
* ===================================================================
|
||
* Drives every .hub-carousel on the page (Hub "What you can do here"
|
||
* and the "ways to be part of MSOS" slider). The track is built from
|
||
* three identical .hub-group copies so the scroll loops seamlessly in
|
||
* both directions; auto-scroll is JS-driven (not a CSS animation) so it
|
||
* can co-exist with click-and-drag and touch swiping.
|
||
*/
|
||
function setupHubCarousel() {
|
||
const carousels = document.querySelectorAll('.hub-carousel');
|
||
if (!carousels.length) return;
|
||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
const LOOP_MS = 55000; // time to travel one group width (matches old 55s marquee)
|
||
|
||
carousels.forEach(function (carousel) {
|
||
if (carousel.dataset.carouselInit) return;
|
||
carousel.dataset.carouselInit = '1';
|
||
|
||
const scroller = carousel.querySelector('.hub-uses');
|
||
const track = carousel.querySelector('.hub-track');
|
||
if (!scroller || !track) return;
|
||
const firstGroup = track.querySelector('.hub-group');
|
||
if (!firstGroup) return;
|
||
|
||
// Need 3 identical groups so a full neighbour is always visible at each wrap point.
|
||
while (track.querySelectorAll('.hub-group').length < 3) {
|
||
const clone = firstGroup.cloneNode(true);
|
||
clone.setAttribute('aria-hidden', 'true');
|
||
track.appendChild(clone);
|
||
}
|
||
|
||
const cards = firstGroup.children;
|
||
const cardCount = cards.length;
|
||
if (!cardCount) return;
|
||
|
||
// --- Position dots ---
|
||
const dotsWrap = document.createElement('div');
|
||
dotsWrap.className = 'hub-dots';
|
||
dotsWrap.setAttribute('role', 'tablist');
|
||
dotsWrap.setAttribute('aria-label', 'Carousel position');
|
||
const dotEls = [];
|
||
for (let i = 0; i < cardCount; i++) {
|
||
const b = document.createElement('button');
|
||
b.type = 'button';
|
||
b.className = 'hub-dot';
|
||
b.setAttribute('aria-label', 'Go to item ' + (i + 1));
|
||
b.addEventListener('click', (function (idx) {
|
||
return function () { goTo(idx); };
|
||
})(i));
|
||
dotsWrap.appendChild(b);
|
||
dotEls.push(b);
|
||
}
|
||
carousel.appendChild(dotsWrap);
|
||
|
||
// --- Measurements (recomputed on resize / image load) ---
|
||
let groupWidth = 0, step = 0;
|
||
function measure() {
|
||
groupWidth = firstGroup.offsetWidth;
|
||
step = cardCount > 1 ? (cards[1].offsetLeft - cards[0].offsetLeft) : groupWidth;
|
||
if (!step) step = groupWidth / cardCount;
|
||
}
|
||
|
||
// Keep scroll within the middle copy [groupWidth, 2*groupWidth) so it loops both ways.
|
||
function wrap() {
|
||
if (!groupWidth) return;
|
||
if (scroller.scrollLeft >= 2 * groupWidth) scroller.scrollLeft -= groupWidth;
|
||
else if (scroller.scrollLeft < groupWidth) scroller.scrollLeft += groupWidth;
|
||
}
|
||
|
||
function updateDots() {
|
||
if (!step || !groupWidth) return;
|
||
const within = ((scroller.scrollLeft % groupWidth) + groupWidth) % groupWidth;
|
||
const active = Math.round(within / step) % cardCount;
|
||
for (let i = 0; i < cardCount; i++) {
|
||
const dist = Math.min(Math.abs(i - active), cardCount - Math.abs(i - active));
|
||
dotEls[i].classList.toggle('is-active', i === active);
|
||
dotEls[i].classList.toggle('is-near', dist === 1);
|
||
dotEls[i].classList.toggle('is-far', dist >= 2);
|
||
}
|
||
}
|
||
|
||
function reset() {
|
||
measure();
|
||
if (groupWidth) scroller.scrollLeft = groupWidth;
|
||
updateDots();
|
||
}
|
||
reset();
|
||
window.addEventListener('load', reset);
|
||
window.addEventListener('resize', function () { measure(); wrap(); updateDots(); });
|
||
|
||
// --- Jump to a specific card when a dot is clicked ---
|
||
// Custom eased scroll (not native smooth) so it never fights the
|
||
// rAF auto-scroll loop. Target always sits in the middle copy
|
||
// [groupWidth, 2*groupWidth), so wrap() stays a no-op during it.
|
||
let programmatic = 0; // ms remaining where auto-scroll yields
|
||
let animId = 0;
|
||
function goTo(idx) {
|
||
if (!step) measure();
|
||
const start = scroller.scrollLeft;
|
||
const target = groupWidth + idx * step;
|
||
const dur = 450, t0 = performance.now();
|
||
const ease = function (t) { return 1 - Math.pow(1 - t, 3); };
|
||
cancelAnimationFrame(animId);
|
||
programmatic = dur + 80;
|
||
(function anim(now) {
|
||
const t = Math.min(1, (now - t0) / dur);
|
||
scroller.scrollLeft = start + (target - start) * ease(t);
|
||
updateDots();
|
||
if (t < 1) animId = requestAnimationFrame(anim);
|
||
})(t0);
|
||
}
|
||
|
||
// --- Auto-scroll loop ---
|
||
let paused = false;
|
||
let dragging = false;
|
||
let last = null;
|
||
function tick(ts) {
|
||
if (last == null) last = ts;
|
||
const dt = ts - last;
|
||
last = ts;
|
||
if (!reduceMotion && !paused && programmatic <= 0 && groupWidth) {
|
||
scroller.scrollLeft += (groupWidth / LOOP_MS) * dt;
|
||
}
|
||
if (programmatic > 0) programmatic -= dt;
|
||
wrap();
|
||
updateDots();
|
||
requestAnimationFrame(tick);
|
||
}
|
||
requestAnimationFrame(tick);
|
||
|
||
// Pause on hover (desktop).
|
||
carousel.addEventListener('mouseenter', function () { paused = true; });
|
||
carousel.addEventListener('mouseleave', function () { if (!dragging) paused = false; });
|
||
|
||
// --- Click-and-drag (mouse) + pause during touch (native scroll handles the swipe) ---
|
||
let lastX = 0;
|
||
scroller.addEventListener('pointerdown', function (e) {
|
||
paused = true;
|
||
if (e.pointerType !== 'mouse') return; // touch/pen: let the browser scroll natively
|
||
dragging = true;
|
||
lastX = e.clientX;
|
||
scroller.classList.add('is-dragging');
|
||
try { scroller.setPointerCapture(e.pointerId); } catch (err) {}
|
||
});
|
||
scroller.addEventListener('pointermove', function (e) {
|
||
if (!dragging) return;
|
||
const dx = e.clientX - lastX;
|
||
lastX = e.clientX;
|
||
scroller.scrollLeft -= dx;
|
||
wrap();
|
||
updateDots();
|
||
e.preventDefault();
|
||
});
|
||
function endDrag(e) {
|
||
if (dragging) {
|
||
dragging = false;
|
||
scroller.classList.remove('is-dragging');
|
||
try { scroller.releasePointerCapture(e.pointerId); } catch (err) {}
|
||
}
|
||
// Resume auto-scroll unless the cursor is still hovering the carousel.
|
||
if (!carousel.matches(':hover')) paused = false;
|
||
}
|
||
scroller.addEventListener('pointerup', endDrag);
|
||
scroller.addEventListener('pointercancel', endDrag);
|
||
|
||
// Prevent the browser's native image drag-ghost while dragging.
|
||
track.addEventListener('dragstart', function (e) { e.preventDefault(); });
|
||
});
|
||
}
|
||
|
||
/**
|
||
* ===================================================================
|
||
* ABOUT US TEAM HERO (auto-rotating crossfade slideshow)
|
||
* ===================================================================
|
||
*/
|
||
function setupAboutHero() {
|
||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||
document.querySelectorAll('.about-hero').forEach(function (hero) {
|
||
const slides = hero.querySelectorAll('.about-hero-slide');
|
||
if (slides.length < 2) return;
|
||
let i = 0;
|
||
setInterval(function () {
|
||
slides[i].classList.remove('is-active');
|
||
i = (i + 1) % slides.length;
|
||
slides[i].classList.add('is-active');
|
||
}, 4500);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* ===================================================================
|
||
* 8. ZAGON FUNKCIJ PO NALOŽITVI STRANI
|
||
* ===================================================================
|
||
*/
|
||
highlightActiveLink();
|
||
setupLanguageSwitcher();
|
||
setupLanguageSuggestion();
|
||
setupNewsletter();
|
||
setupHubCarousel();
|
||
setupAboutHero();
|
||
setupLightbox();
|
||
|
||
}); |