Merge pull request 'Improve article TOC, sticky header, and cross-language anchors' (#1) from kristijan-dev into develop
Deploy to Production on Develop Push / deploy (push) Successful in 1m35s
Details
Deploy to Production on Develop Push / deploy (push) Successful in 1m35s
Details
Reviewed-on: #1
This commit is contained in:
commit
6c022d47d9
|
|
@ -15,7 +15,9 @@
|
|||
|
||||
/* 1. Glavni kontejner za glavo */
|
||||
.dropdown-header-navigation {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background: #FFFFFF;
|
||||
|
|
|
|||
|
|
@ -51,15 +51,36 @@
|
|||
width: 288px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 120px;
|
||||
height: calc(100vh - 240px);
|
||||
top: 112px;
|
||||
/* 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 {
|
||||
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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* No flex centering here: centering inside a scroll container pushed the first
|
||||
item out of reach at 100% zoom / short viewports. Content now flows from the top. */
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.scrollspy-nav ul {
|
||||
|
|
@ -68,15 +89,16 @@
|
|||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
gap: 8px;
|
||||
border-left: 2px solid #EAECF0;
|
||||
}
|
||||
|
||||
.scrollspy-nav li a {
|
||||
display: block;
|
||||
padding: 12px 24px;
|
||||
padding: 8px 20px;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
color: #667085;
|
||||
border-left: 2px solid transparent;
|
||||
|
|
|
|||
190
main.js
190
main.js
|
|
@ -275,39 +275,134 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
try {
|
||||
const articlePage = document.querySelector('.article-page');
|
||||
if (articlePage) {
|
||||
const scrollspyNav = document.querySelector('.scrollspy-nav');
|
||||
const mobileHeader = document.querySelector('.mobile-article-header');
|
||||
const mobileTitle = document.getElementById('mobile-article-title');
|
||||
const mobileDropdown = document.querySelector('.mobile-scrollspy-dropdown');
|
||||
const mobileLinksContainer = document.getElementById('mobile-scrollspy-links');
|
||||
const progressBar = document.querySelector('.progress-bar');
|
||||
|
||||
const headings = Array.from(articlePage.querySelectorAll('.article-body h2'));
|
||||
const desktopNavLinks = scrollspyNav ? Array.from(scrollspyNav.querySelectorAll('a')) : [];
|
||||
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();
|
||||
|
||||
// --- Dinamično ustvarjanje mobilnega spustnega seznama ---
|
||||
if (headings.length > 0 && mobileLinksContainer) {
|
||||
// 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 = heading.textContent;
|
||||
link.href = '#' + heading.id;
|
||||
link.textContent = navLabel(heading);
|
||||
listItem.appendChild(link);
|
||||
mobileLinksContainer.appendChild(listItem);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Logika za odpiranje/zapiranje mobilnega spustnega seznama ---
|
||||
|
||||
// --- 5g. Odpiranje/zapiranje mobilnega spustnega seznama ---
|
||||
if (mobileHeader && mobileDropdown) {
|
||||
mobileHeader.addEventListener('click', (e) => {
|
||||
// Preprečimo, da se meni zapre, če kliknemo na povezavo znotraj njega
|
||||
if (!e.target.closest('a')) {
|
||||
mobileHeader.classList.toggle('open');
|
||||
mobileDropdown.classList.toggle('open');
|
||||
mobileHeader.classList.toggle('open');
|
||||
mobileDropdown.classList.toggle('open');
|
||||
}
|
||||
});
|
||||
|
||||
// Dodamo dogodek za zapiranje menija ob kliku na povezavo
|
||||
mobileDropdown.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
mobileHeader.classList.remove('open');
|
||||
|
|
@ -316,56 +411,55 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
}
|
||||
|
||||
// --- Logika za Progress Bar ---
|
||||
// --- 5h. Progress Bar ---
|
||||
function updateProgressBar() {
|
||||
if (!progressBar) return;
|
||||
const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const scrollTop = window.scrollY;
|
||||
|
||||
if (scrollableHeight > 0) {
|
||||
const progress = (scrollTop / scrollableHeight) * 100;
|
||||
progressBar.style.width = `${progress}%`;
|
||||
} else {
|
||||
progressBar.style.width = '0%';
|
||||
}
|
||||
progressBar.style.width = scrollableHeight > 0
|
||||
? `${(scrollTop / scrollableHeight) * 100}%`
|
||||
: '0%';
|
||||
}
|
||||
|
||||
// --- Logika za Scrollspy ---
|
||||
// --- 5i. Scrollspy (posodobi navigacijo, naslov in URL sidro) ---
|
||||
if (headings.length > 0) {
|
||||
let activeId = null;
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
const id = entry.target.getAttribute('id');
|
||||
const desktopNavLink = desktopNavLinks.find(link => link.getAttribute('href') === `#${id}`);
|
||||
const mobileNavLink = mobileLinksContainer ? mobileLinksContainer.querySelector(`a[href="#${id}"]`) : null;
|
||||
if (!entry.isIntersecting) return;
|
||||
const id = entry.target.id;
|
||||
if (id === activeId) return;
|
||||
activeId = id;
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
// Posodobi namizno navigacijo
|
||||
if (desktopNavLink) {
|
||||
desktopNavLinks.forEach(link => link.classList.remove('active'));
|
||||
desktopNavLink.classList.add('active');
|
||||
}
|
||||
// Posodobi mobilni naslov in aktivno povezavo v spustnem seznamu
|
||||
if (mobileTitle) {
|
||||
mobileTitle.textContent = entry.target.textContent;
|
||||
}
|
||||
// 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 -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 => {
|
||||
observer.observe(heading);
|
||||
});
|
||||
headings.forEach(heading => observer.observe(heading));
|
||||
}
|
||||
|
||||
// Dodaj poslušalca dogodkov za vrstico napredka
|
||||
window.addEventListener('scroll', updateProgressBar);
|
||||
updateProgressBar(); // Klic ob nalaganju strani
|
||||
updateProgressBar();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in Article Scrollspy/Progress Bar setup:', error);
|
||||
|
|
|
|||
Loading…
Reference in New Issue