Compare commits

..

No commits in common. "6c022d47d94ecc8da1fdb69f4dbc91d3c1b07a4e" and "d972e45c2f4584fd250062f5d1a3365cfa296edc" have entirely different histories.

3 changed files with 55 additions and 173 deletions

View File

@ -15,9 +15,7 @@
/* 1. Glavni kontejner za glavo */ /* 1. Glavni kontejner za glavo */
.dropdown-header-navigation { .dropdown-header-navigation {
position: fixed; position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 80px; height: 80px;
background: #FFFFFF; background: #FFFFFF;

View File

@ -51,36 +51,15 @@
width: 288px; width: 288px;
flex-shrink: 0; flex-shrink: 0;
position: sticky; position: sticky;
top: 112px; top: 120px;
/* max-height (not fixed height) so short lists size to content and never clip; height: calc(100vh - 240px);
the list only scrolls when the viewport is genuinely too short. */
max-height: calc(100vh - 140px);
display: flex;
flex-direction: column;
}
/* Article title above the table of contents, so the reader always knows
which article they are reading. Injected by main.js from the page <h1>. */
.article-sidebar-title {
flex-shrink: 0;
margin: 0 0 16px 0;
padding: 0 0 16px 20px;
font-size: 16px;
font-weight: 600;
line-height: 24px;
color: #101828;
border-bottom: 1px solid #EAECF0;
} }
.scrollspy-nav { .scrollspy-nav {
/* Fill the remaining sidebar height and scroll internally when needed. height: 100%;
min-height:0 lets a flex child actually shrink so overflow works. */
flex: 1 1 auto;
min-height: 0;
overflow-y: auto; overflow-y: auto;
/* No flex centering here: centering inside a scroll container pushed the first display: flex;
item out of reach at 100% zoom / short viewports. Content now flows from the top. */ align-items: center;
padding: 4px 0;
} }
.scrollspy-nav ul { .scrollspy-nav ul {
@ -89,16 +68,15 @@
margin: 0; margin: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 40px;
border-left: 2px solid #EAECF0; border-left: 2px solid #EAECF0;
} }
.scrollspy-nav li a { .scrollspy-nav li a {
display: block; display: block;
padding: 8px 20px; padding: 12px 24px;
text-decoration: none; text-decoration: none;
font-size: 14px; font-size: 16px;
line-height: 20px;
font-weight: 500; font-weight: 500;
color: #667085; color: #667085;
border-left: 2px solid transparent; border-left: 2px solid transparent;

186
main.js
View File

@ -275,134 +275,39 @@ document.addEventListener('DOMContentLoaded', function() {
try { try {
const articlePage = document.querySelector('.article-page'); const articlePage = document.querySelector('.article-page');
if (articlePage) { if (articlePage) {
const articleContainer = articlePage.querySelector('.article-container'); const scrollspyNav = document.querySelector('.scrollspy-nav');
const articleBody = articlePage.querySelector('.article-body'); const mobileHeader = document.querySelector('.mobile-article-header');
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'); const mobileTitle = document.getElementById('mobile-article-title');
if (mobileTitle && !mobileTitle.textContent.trim()) { const mobileDropdown = document.querySelector('.mobile-scrollspy-dropdown');
mobileTitle.textContent = articleTitle; // known at load; scrollspy refines it
}
const mobileDropdown = articlePage.querySelector('.mobile-scrollspy-dropdown');
const mobileLinksContainer = document.getElementById('mobile-scrollspy-links'); const mobileLinksContainer = document.getElementById('mobile-scrollspy-links');
const progressBar = articlePage.querySelector('.progress-bar'); const progressBar = document.querySelector('.progress-bar');
const desktopNavLinks = desktopUl ? Array.from(desktopUl.querySelectorAll('a')) : [];
const headings = Array.from(articlePage.querySelectorAll('.article-body h2'));
const desktopNavLinks = scrollspyNav ? Array.from(scrollspyNav.querySelectorAll('a')) : [];
// --- 5f. Napolni mobilni spustni seznam --- // --- Dinamično ustvarjanje mobilnega spustnega seznama ---
if (headings.length > 0 && mobileLinksContainer && mobileLinksContainer.children.length === 0) { if (headings.length > 0 && mobileLinksContainer) {
headings.forEach(heading => { headings.forEach(heading => {
const listItem = document.createElement('li'); const listItem = document.createElement('li');
const link = document.createElement('a'); const link = document.createElement('a');
link.href = '#' + heading.id; link.href = `#${heading.id}`;
link.textContent = navLabel(heading); link.textContent = heading.textContent;
listItem.appendChild(link); listItem.appendChild(link);
mobileLinksContainer.appendChild(listItem); mobileLinksContainer.appendChild(listItem);
}); });
} }
// --- 5g. Odpiranje/zapiranje mobilnega spustnega seznama --- // --- Logika za odpiranje/zapiranje mobilnega spustnega seznama ---
if (mobileHeader && mobileDropdown) { if (mobileHeader && mobileDropdown) {
mobileHeader.addEventListener('click', (e) => { mobileHeader.addEventListener('click', (e) => {
// Preprečimo, da se meni zapre, če kliknemo na povezavo znotraj njega
if (!e.target.closest('a')) { if (!e.target.closest('a')) {
mobileHeader.classList.toggle('open'); mobileHeader.classList.toggle('open');
mobileDropdown.classList.toggle('open'); mobileDropdown.classList.toggle('open');
} }
}); });
// Dodamo dogodek za zapiranje menija ob kliku na povezavo
mobileDropdown.querySelectorAll('a').forEach(link => { mobileDropdown.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => { link.addEventListener('click', () => {
mobileHeader.classList.remove('open'); mobileHeader.classList.remove('open');
@ -411,55 +316,56 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// --- 5h. Progress Bar --- // --- Logika za Progress Bar ---
function updateProgressBar() { function updateProgressBar() {
if (!progressBar) return; if (!progressBar) return;
const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollTop = window.scrollY; const scrollTop = window.scrollY;
progressBar.style.width = scrollableHeight > 0
? `${(scrollTop / scrollableHeight) * 100}%` if (scrollableHeight > 0) {
: '0%'; const progress = (scrollTop / scrollableHeight) * 100;
progressBar.style.width = `${progress}%`;
} else {
progressBar.style.width = '0%';
}
} }
// --- 5i. Scrollspy (posodobi navigacijo, naslov in URL sidro) --- // --- Logika za Scrollspy ---
if (headings.length > 0) { if (headings.length > 0) {
let activeId = null;
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => {
if (!entry.isIntersecting) return; const id = entry.target.getAttribute('id');
const id = entry.target.id;
if (id === activeId) return;
activeId = id;
// Namizna navigacija
const desktopNavLink = desktopNavLinks.find(link => link.getAttribute('href') === `#${id}`); const desktopNavLink = desktopNavLinks.find(link => link.getAttribute('href') === `#${id}`);
if (desktopNavLink) { const mobileNavLink = mobileLinksContainer ? mobileLinksContainer.querySelector(`a[href="#${id}"]`) : null;
desktopNavLinks.forEach(link => link.classList.remove('active'));
desktopNavLink.classList.add('active'); if (entry.isIntersecting) {
} // Posodobi namizno navigacijo
// Mobilni naslov + aktivna povezava if (desktopNavLink) {
if (mobileTitle) mobileTitle.textContent = navLabel(entry.target); desktopNavLinks.forEach(link => link.classList.remove('active'));
if (mobileLinksContainer) { desktopNavLink.classList.add('active');
const mobileNavLink = mobileLinksContainer.querySelector(`a[href="#${id}"]`); }
// Posodobi mobilni naslov in aktivno povezavo v spustnem seznamu
if (mobileTitle) {
mobileTitle.textContent = entry.target.textContent;
}
if (mobileNavLink) { if (mobileNavLink) {
mobileLinksContainer.querySelectorAll('a').forEach(link => link.classList.remove('active')); mobileLinksContainer.querySelectorAll('a').forEach(link => link.classList.remove('active'));
mobileNavLink.classList.add('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 rootMargin: "-100px 0px -50% 0px" // Sproži, ko je naslov v zgornjem delu zaslona
}); });
headings.forEach(heading => observer.observe(heading)); headings.forEach(heading => {
observer.observe(heading);
});
} }
// Dodaj poslušalca dogodkov za vrstico napredka
window.addEventListener('scroll', updateProgressBar); window.addEventListener('scroll', updateProgressBar);
updateProgressBar(); updateProgressBar(); // Klic ob nalaganju strani
} }
} catch (error) { } catch (error) {
console.error('Error in Article Scrollspy/Progress Bar setup:', error); console.error('Error in Article Scrollspy/Progress Bar setup:', error);