Improve article TOC, sticky header, and cross-language anchors #1
|
|
@ -15,7 +15,9 @@
|
||||||
|
|
||||||
/* 1. Glavni kontejner za glavo */
|
/* 1. Glavni kontejner za glavo */
|
||||||
.dropdown-header-navigation {
|
.dropdown-header-navigation {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
|
|
|
||||||
|
|
@ -51,15 +51,36 @@
|
||||||
width: 288px;
|
width: 288px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 120px;
|
top: 112px;
|
||||||
height: calc(100vh - 240px);
|
/* max-height (not fixed height) so short lists size to content and never clip;
|
||||||
|
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 {
|
||||||
height: 100%;
|
/* Fill the remaining sidebar height and scroll internally when needed.
|
||||||
|
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;
|
||||||
display: flex;
|
/* No flex centering here: centering inside a scroll container pushed the first
|
||||||
align-items: center;
|
item out of reach at 100% zoom / short viewports. Content now flows from the top. */
|
||||||
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollspy-nav ul {
|
.scrollspy-nav ul {
|
||||||
|
|
@ -68,15 +89,16 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 40px;
|
gap: 8px;
|
||||||
border-left: 2px solid #EAECF0;
|
border-left: 2px solid #EAECF0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollspy-nav li a {
|
.scrollspy-nav li a {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 12px 24px;
|
padding: 8px 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
border-left: 2px solid transparent;
|
border-left: 2px solid transparent;
|
||||||
|
|
|
||||||
172
main.js
172
main.js
|
|
@ -275,39 +275,134 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
try {
|
try {
|
||||||
const articlePage = document.querySelector('.article-page');
|
const articlePage = document.querySelector('.article-page');
|
||||||
if (articlePage) {
|
if (articlePage) {
|
||||||
const scrollspyNav = document.querySelector('.scrollspy-nav');
|
const articleContainer = articlePage.querySelector('.article-container');
|
||||||
const mobileHeader = document.querySelector('.mobile-article-header');
|
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');
|
const mobileTitle = document.getElementById('mobile-article-title');
|
||||||
const mobileDropdown = document.querySelector('.mobile-scrollspy-dropdown');
|
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 mobileLinksContainer = document.getElementById('mobile-scrollspy-links');
|
||||||
const progressBar = document.querySelector('.progress-bar');
|
const progressBar = articlePage.querySelector('.progress-bar');
|
||||||
|
const desktopNavLinks = desktopUl ? Array.from(desktopUl.querySelectorAll('a')) : [];
|
||||||
|
|
||||||
const headings = Array.from(articlePage.querySelectorAll('.article-body h2'));
|
// --- 5f. Napolni mobilni spustni seznam ---
|
||||||
const desktopNavLinks = scrollspyNav ? Array.from(scrollspyNav.querySelectorAll('a')) : [];
|
if (headings.length > 0 && mobileLinksContainer && mobileLinksContainer.children.length === 0) {
|
||||||
|
|
||||||
// --- Dinamično ustvarjanje mobilnega spustnega seznama ---
|
|
||||||
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 = heading.textContent;
|
link.textContent = navLabel(heading);
|
||||||
listItem.appendChild(link);
|
listItem.appendChild(link);
|
||||||
mobileLinksContainer.appendChild(listItem);
|
mobileLinksContainer.appendChild(listItem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Logika za odpiranje/zapiranje mobilnega spustnega seznama ---
|
// --- 5g. 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');
|
||||||
|
|
@ -316,56 +411,55 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Logika za Progress Bar ---
|
// --- 5h. 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
|
||||||
if (scrollableHeight > 0) {
|
? `${(scrollTop / scrollableHeight) * 100}%`
|
||||||
const progress = (scrollTop / scrollableHeight) * 100;
|
: '0%';
|
||||||
progressBar.style.width = `${progress}%`;
|
|
||||||
} else {
|
|
||||||
progressBar.style.width = '0%';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Logika za Scrollspy ---
|
// --- 5i. Scrollspy (posodobi navigacijo, naslov in URL sidro) ---
|
||||||
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 => {
|
||||||
const id = entry.target.getAttribute('id');
|
if (!entry.isIntersecting) return;
|
||||||
const desktopNavLink = desktopNavLinks.find(link => link.getAttribute('href') === `#${id}`);
|
const id = entry.target.id;
|
||||||
const mobileNavLink = mobileLinksContainer ? mobileLinksContainer.querySelector(`a[href="#${id}"]`) : null;
|
if (id === activeId) return;
|
||||||
|
activeId = id;
|
||||||
|
|
||||||
if (entry.isIntersecting) {
|
// Namizna navigacija
|
||||||
// Posodobi namizno navigacijo
|
const desktopNavLink = desktopNavLinks.find(link => link.getAttribute('href') === `#${id}`);
|
||||||
if (desktopNavLink) {
|
if (desktopNavLink) {
|
||||||
desktopNavLinks.forEach(link => link.classList.remove('active'));
|
desktopNavLinks.forEach(link => link.classList.remove('active'));
|
||||||
desktopNavLink.classList.add('active');
|
desktopNavLink.classList.add('active');
|
||||||
}
|
}
|
||||||
// Posodobi mobilni naslov in aktivno povezavo v spustnem seznamu
|
// Mobilni naslov + aktivna povezava
|
||||||
if (mobileTitle) {
|
if (mobileTitle) mobileTitle.textContent = navLabel(entry.target);
|
||||||
mobileTitle.textContent = entry.target.textContent;
|
if (mobileLinksContainer) {
|
||||||
}
|
const mobileNavLink = mobileLinksContainer.querySelector(`a[href="#${id}"]`);
|
||||||
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 -50% 0px" // Sproži, ko je naslov v zgornjem delu zaslona
|
rootMargin: "-100px 0px -55% 0px" // Sproži, ko je naslov v zgornjem delu zaslona
|
||||||
});
|
});
|
||||||
|
|
||||||
headings.forEach(heading => {
|
headings.forEach(heading => observer.observe(heading));
|
||||||
observer.observe(heading);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dodaj poslušalca dogodkov za vrstico napredka
|
|
||||||
window.addEventListener('scroll', updateProgressBar);
|
window.addEventListener('scroll', updateProgressBar);
|
||||||
updateProgressBar(); // Klic ob nalaganju strani
|
updateProgressBar();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in Article Scrollspy/Progress Bar setup:', error);
|
console.error('Error in Article Scrollspy/Progress Bar setup:', error);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue