• Jetzt anmelden. Es dauert nur 2 Minuten und ist kostenlos!

Info Onepager Script mit Highlighting der Menüpunkte

Oliver77

Aktives Mitglied
Onepager
Hallo, hier möchte ich ein Script vorstellen, um einen Effekt einzusetzen bei dem die Menüpunkte beim Scrollen hervorgehoben werden.
Für Wordpress empfehle ich das Plugin Custom CSS & JS.
Dort das Script im Footer integrieren.
Das HTML:
HTML:
<li class="menu-item"><a href="#">Seite 1</a></li> erstes Element
<li class="menu-item"><a href="#section-2">Seite 2</a></li>
<li class="menu-item"><a href="#section-3">Seite 3</a></li>
Dann ID's vergeben:
HTML:
 <h2  id="section-2">Seite 2</h2>

Wordpress
HTML:
Individueller Link:
https://meine-seite.de/#mein-anker
HTML:
Individueller Link Startseite:
https://meine-seite.de/#

jQuery
Javascript:
const primaryMenu = "#site-navigation .menu-item a[href*='#']";
    // Die Anker-Selektoren
    const menuItem = "#site-navigation .menu-item";
    // Die Listelemente der Navigation
    const nav = "#site-navigation";
    // Der übergeordnete Selektor, hier das Ul
    const mobileNav = "nav";
    //mobil Navigations-Container
    const marginTop = 0;
    // Offset-Abstand
     const mobileMarginTop = 0;
     // Offset-Abstand mobil
    const speed = 600;
    // Scrollgeschwindigkeit
    const mobile = 630;
    // Breite der Mobilanisicht
Javascript:
(function ($) {
    const primaryMenu = "#site-navigation .menu-item a[href*='#']";
    const menuItem = "#site-navigation .menu-item";
    const nav = "#site-navigation";
    const mobileNav = "nav";
    const marginTop = 0;
    const mobileMarginTop = 0;
    const speed = 600;
    const mobile = 630;
    var lastId;
    var posArray = [];
    var eventFlag = true;
    var lastIndex;
    var indexCount = 0;
    var navHeight;
    
    if ($(window).outerWidth() > mobile && $(document).scrollTop() == 0) {
        $(menuItem + ":first-child").addClass("scrollact");
    }
    $(primaryMenu).click(function (event) {
        let actMarginTop;
        if ($(window).width() < mobile) {
        actMarginTop = mobileMarginTop;
        navHeight = $(mobileNav).outerHeight();
        }
        else {
            actMarginTop = marginTop;
            navHeight = $(nav).outerHeight();
        }
        
        event.preventDefault();
        var id = $(this).prop("hash");
        if (id == "") {
            $('html, body').animate({scrollTop: ((0))}, speed);
        } else if ($(id).length) {
            $('html, body').animate({scrollTop: (($(id).offset().top - navHeight - actMarginTop))}, speed);
        }
    });
    setTimeout(function () {
        pos();
    }, 200);
    $(window).scroll(function () {
        if ($(window).width() > mobile) {
            var scrollPos = $(document).scrollTop();
            $.each(posArray, function (index, value) {
                if (scrollPos > value - 1) {
                    indexCount = index;
                    if (lastIndex < index) {
                        eventFlag = true;
                    }
                }
                if (scrollPos < value + 1) {
                    if (lastIndex == index) {
                        eventFlag = true;
                    }
                }
            });
            $.each(posArray, function (index, value) {
                if (scrollPos > value - 1 && eventFlag) {
                    if (index == indexCount) {
                        lastIndex = index;
                        eventFlag = false;
                        $(menuItem).removeClass("scrollact");
                        $(menuItem).eq(index).addClass("scrollact");
                    }
                }
            });
            if (scrollPos > $(document).height() - 1 - $(window).height()) {
                $(menuItem).removeClass("scrollact");
                $(primaryMenu).eq(lastId).parent("li").addClass("scrollact");
                eventFlag = true;
            }
        }
    });
    function pos() {
        posArray = [];
        navHeight = $(nav).outerHeight();
        $(primaryMenu).each(function (index) {
            var id = $(this).prop("hash");
            if (id == "") {
                posArray.push(0);
            } else if ($(id).length) {
                posArray.push($(id).offset().top - navHeight - marginTop);
                lastId = index;
            }
        });
    }
    $(window).resize(function () {
             if ($(window).outerWidth() > mobile) {
            pos();
        } else {
            $(menuItem).removeClass("scrollact");
        }
    });
})(jQuery);
CSS
CSS:
.scrollact {
background:#f00;
}

Eine Demo gibt es hier:
https://mg-otterson.de/fileadmin/scroll-mark/

Beispiel auf Codepen:
https://codepen.io/Oliver7777/pen/MWROYPY
 
Funktioniert sehr schön.
Ich habe mir dein Script man angeschaut und habe nun einige Fragen und Anmerkungen:

Fragen:
  1. Ich muss das einfach fragen: Warum hast du jQuery verwendet? Ich verstehe es so, dass dies ein Script ist, dass andere in ihere Seite einbinden sollen. Dann würde ich versuchen die Abhängigkeiten zu anderen Scripten so klein wie möglich halten. Dein Code benötigt meiner Meinung nach kein jQuery.
  2. Beim Durchschauen deines Codes ist mir der Teil mit dem eventFlag und indexCount aufgefallen und ich kann nicht verstehen, wieso du das so kompliziert gelößt hast. Kannst du das kurz erklären, eventuell überseh' ich ja was?
  3. Wieso hast du den Klick Listener auf das primaryMenu Item mit in das Highlight Script gepackt? Es hat ja nicht wirklich etwas mit dem highlighting zu tun und wenn ein User dieses Feature nicht möchte, kann er es nicht ausschalten.
  4. Wieso hast du den setTimeout Aufruf hinzugefügt? Ich kann mir das nur so erklären, dass es dafür gedacht ist, die Initilalisierung zu verzögern. Dafür könntest du aber auch DOMContentLoaded event oder $.ready
    verwenden.

Anmerkungen:
(Diese Anmerkungen sind nur Vorschläge, die dir eventuell helfen können den Code leserlicher wartbarer und eventuell auch performanter zu gestalten.)

  1. Mir ist aufgefallen, dass das Heighlight nicht aktualisiert wird, wenn man von der Mobile Ansicht zur Desktop Ansicht wechselt. Das würde ich mir nochmal anschauen.
  2. Ich würde dir empfehlen, die Entscheidung ob du dich in einem Mobilen Breakpoint befindest oder nicht, mit windows.matchMedia abzutesten. Das bietet die ein einheitliches Interface dafür. Außerdem kannst du auch einen change Listener auf die MediaQueryList die du zurück bekommst setzen, was dein resize Listener ersetzen könnte.
  3. Ich würde dir empfehlen, den Klassennamen scrollact in einer Konstanten zu speichern, damit sie zentral geändert werden kann. Und eventuell dem User des Scriptes ermöglich diesen Namen selber ändern zu können.
  4. ich persönlich verwende immer variablen für boolische Ausdrücke, was die Übersichtlichkeit bei Konditionalen Abfragen erhöht. sowas wie const isScrollPosBeneathTheCurrentPos = scrollPos > value - 1; oder const isScrollPosAtTheEnd = scrollPos > $(document).height() - $(window).height() - 1;.
  5. In hinblick darauf, dass dein Script von einem Dritten eingebunden werden soll, würde ich dir empfehlen, dass dein Script einen globalen namespace auf dem Window Objekt definiert, wie window.scrollMitHIghlight oder so mit mindestens einer Methode um das Verhalten zu initialisieren und eventuell einer um das Verhalten wieder zu deaktivieren.
  6. Wenn du den Klick Listener auf dem primaryMenu behalten möchtest, würde ich dir scrollTo anstelle von .animate({scrollTop}) empfehlen, da das die native Methode zum Scrollen ist.
  7. Scroll Handler (und auch resize Handler) sollte man eigentlich immer debouncen, damit die Handler nicht zu oft aufgerufen werden und die Webseite zum Stocken bringen. Debouncing and Throttling Explained Through Examples hilft um das Thema zu verstehen.

Grüße
Andreas
 
Hallo danke für das ausführliche Feedback,
ich habe jQuery genommen, weil ich das Script für Wordpress verwende, und da ist es ja standardmäßig inkludiert- also warum nicht jQuery nehmen.

Ich habe SetTimeout verwendet, weil .offset().top einen falschen Wert liefert, wenn es sofort ausgelesen wird.
Gibt es da eine andere Lösung?
document.ready hilft auch nicht weiter.
Ich werde noch mal deinen Post durchgehen.
 
also warum nicht jQuery nehmen.
Wenn halt jemand das Script verwenden möchte und kein Wordpress verwendet (oder schon jQuery geladen hat) muss derjenige zusätzlich noch jQuery laden, was für so ein kleines Script ein ziemlicher overhead ist.

weil .offset().top einen falschen Wert liefert, wenn es sofort ausgelesen wird.
Ich habe mir nochmal deinen Beispielcode durchgelesen und du hast recht.
Der Grund weshalb die Top-Werte nicht richtig ausgelesen werden liegt an den Bildern. Die sind zum Zeitpunkt von DOMContentLoaded und auch von $.ready noch nicht geladen und da deine img Tags keine width und height Werte hat, wird der Platz auch nicht freigehalten.
Eine Möglichkeit auf die Bilder zu warten wäre das load event auf dem window abzufangen.
 
Man könnte prüfen, ob man auch mit dem IntersectionObserver zum Ziel kommt.
Eine andere Alternative könnte die Verwendung von getBoundingClientRect sein. Dann bekommt man die Position eines Containers relativ zum Viewport und kann durch Prüfen von top und bottom ermitteln ob er sichtbar ist, ohne die Scrollposition auswerten zu müssen. Möglicher Weise kann das den Code vereinfachen.
 
Eine andere Alternative könnte die Verwendung von getBoundingClientRect sein
Das stimmt, aber das würde nicht viel an dem bisheringen Prinzip ändern, da man dann an statt über die Top-Werte zu iterieren, über die Überschriften selber iterieren müsste.

Den Bestehenden Code könnte man so zum Beispiel vereinfachen:
Javascript:
let newIndex = lastIndex;
$.each(posArray, function (index, value) {
    const isScrollPosBeneathTheCurrentPos = scrollPos > value;
    if (isScrollPosBeneathTheCurrentPos) {
        newIndex = index;
    }
});

if (lastIndex !== newIndex) {
    $(menuItem).removeClass("scrollact");
    $(menuItem).eq(lastIndex).addClass("scrollact");
    lastIndex = newIndex;
}

Oder, wenn man auf lastIndex verzichten möchte so in etwa:
Javascript:
let newIndex = 0;
$.each(posArray, function (index, value) {
    const isScrollPosBeneathTheCurrentPos = scrollPos > value;
    if (isScrollPosBeneathTheCurrentPos) {
        newIndex = index;
    }
});

const newtItem = $(menuItem).eq(newIndex);
const isNewItemHighlighted = newtItem.hasClass("scrollact");

if (!isNewItemHighlighted) {
    const lastItem = $(menuItem).filter(".scrollact");
    lastItem.removeClass("scrollact");
    newtItem.addClass("scrollact");
}

Die Variante mit getBoundingClientRect würde im Prinzip genauso Funktionieren.
 
*UPDATE*
Ich habe es erweitert, so dass auch Seiten, die nicht zum Onepager gehören, korrekt verwendet werden können. (Im Beispiel die Impressumsseite)
Außerdem stelle ich eine Wordpress-Implementierung vor.
Onepager mit Highlights
 
Zurück
Oben