Manuelles Scrollen bei WebComponents funktioniert initial nicht

Kokujou

Lieutenant
Registriert
Dez. 2017
Beiträge
929
Hi Leute,

ich hab ein kleines Projekt aufgezogen und arbeite da mit WebComponents. So massiv, dass quasi das ganze Projekt eine Sammlung aus WebComponents ist.

Nun habe ich aber ein Problem, und ein Bild sagt mehr als tausend Worte:
1623251323603.png


ich habe einen Scroll-Select gebaut. Das bedeutet dass ich das native Scroll-behaviour überschreibe und nur gesteuert über bestimmte events wie z.B. drag und klick die position der beiden Striche veränderte, die das aktuell ausgewählte Item anzeigen. Coole Idee oder? Funktionieren tut es ganz gut. Allerdings hab ich nun ein Problem. Und zwar scheint das initial einfach nicht zu funktionieren.

Meine Konsole ist Lupenrein, es kann also nicht sein dass er einfach nur irgendwelche Argumente nicht geladen hat. Für die Webcomponents benutze ich LitHTML, und um sicher zu stellen, dass das Scrollen zum initial aktiven Element immer nach dem Rendern passiert überschreibe ich die updated() methode.

das ganze sieht dann so aus:
Code:
 this.currentItemIndex = this.options.findIndex((x) => x == this.value);
        scrollIntoParentView(this.scrollChildren[this.currentItemIndex], this.scrollContainer);

und die scrollIntoParentView sieht so aus
Code:
/**
 * @param {HTMLElement} element
 * @param {HTMLElement} parent
 */
export function scrollIntoParentView(element, parent) {
    var targetLeft = element.offsetLeft - parent.offsetWidth / 2;
    var targetTop = element.offsetTop - parent.offsetHeight / 2 + element.offsetHeight / 2;
    parent.scrollTo({ left: targetLeft, top: targetTop, behavior: 'smooth' });
}

den Status den ihr seht, also dass das erste Element zu... sagen wir mal 20% nicht im Rahmen der beiden Linien ist ist eigentlich gar nicht möglich. ich nehme also an dass er ohne dass irgendeines der custom scroll events fruchtet, einfach scrollTop = 0 hat.

wär cool, wenn ihr mir etwas unter die Arme greifen könntet :)
 
Hier würde ich doch mal mit nem Breakpoint oder alternativ dem debugger-Statement rangehen.
Sollte es da dann, warum auch immer, nicht auftreten: console.log.

So kannst du dein "scrollTop = 0" verifizieren.
 
also das scrollTop ist natürlich 0. Die Frage ist, warum ist es null obwohl mein kompletter code ausgeführt wird? Und wie kriege ich es ordentlich hin?

Ich persönlich glaube irgendwo stimmt mein liefe-cycle noch nicht... vielleicht kommen die scroll events ja viel später nach dem rendern als die updated methode? Der Inhalt der Scroll-area ist auf jeden Fall statisch.

Aber es passiert auch manchmal wenn ich z.B. die konsole öffne. Plötzlich scrollt er er hoch sodass der oben beschriebene status entsteht. Ich wüsste nicht was ich hier debuggen soll. Durchs debuggen zeigt sich sowas ja nichtmal. Denn sobald ich drüber gehe, das element refresht wird oder ich es im inspektor markiere ist der fehler ja sofort wieder weg, weil dann das updated getriggert wird.

Ich hatte gehofft irgendjemand hätte sofort ein klicken im kopf gehört und gesagt "ach, da gibts doch die document.onSomeNiceCalculationsFinished event oder ein document.onAllMyEventMathIsrecalculatedForSomeReason event. Ja gut das klingt jetzt natürlich doof, aber ich glaube mir fehlt nur der richtige Zeitpunkt, irgendein event, irgendein callback, an dass ich das scrolling anhängen muss...
 
hab ich ja alles schon versucht, aber es kommt einfach nichts... gescrollt wird nicht. nicht von mir zumindest... ich hab sogar schon versucht mit einem javascript timeout von einer Sekunde den scroll zum ersten Item zu verzögern, auch das ist aber nicht so hundertprozent...

ich meine, wie würdet ihr es denn machen? Ich meine wenn ihr das Scrollen von A bis Z selbst implementieren wolltet. Mit nativem Javascript

es gibt ja auch immer noch die Möglichkeit die ScrollTop Property zu binden ... aber das ist auch nicht sonderlich sexy, da es so alles andere als smooth ist und nur für fixe werte aber nicht für transitions gilt wie dieses "ziehen zum scrollen" dass ich auch implementiert habe...

wobei es vielleicht oder auch nicht sogar möglich wäre, aber ich zweifle ein bischen daran dass 60 re-renderings meines HTML codes pro sekunde wirklich so eine gute Idee wären... es klingt auf jeden Fall gefährlich. oder was meint ihr?
 
Diese Scroll-Geschichten sind meistens etwas unzuverlässig weil sie ständig von Reflows beeinflusst werden. Probier mal dein Scrollding alleine auf einer eigenen Seite und schau wie es sich dort verhält. Wenn das isoliert ist tust du dir auch mit dem Debugging leichter.
Was mir noch einfällt: Probier mal die height des Containers zu fixieren, size changes pfuschen da auch manchmal rein.
 
Vielleicht ist hier eine Transformation eher etwas für dich, da sind 60 Draws pro Sekunde auch definitiv kein Problem und wird automatisch vom Browser mit einer Transition erledigt.
 
floq0r schrieb:
Diese Scroll-Geschichten sind meistens etwas unzuverlässig weil sie ständig von Reflows beeinflusst werden. Probier mal dein Scrollding alleine auf einer eigenen Seite und schau wie es sich dort verhält. Wenn das isoliert ist tust du dir auch mit dem Debugging leichter.
Was mir noch einfällt: Probier mal die height des Containers zu fixieren, size changes pfuschen da auch manchmal rein.
die Höhe und Breite ist tatsächlich schon fixiert, sogar ziemlich heftig mit max und min-width

Bagbag schrieb:
Vielleicht ist hier eine Transformation eher etwas für dich, da sind 60 Draws pro Sekunde auch definitiv kein Problem und wird automatisch vom Browser mit einer Transition erledigt.
Ja, Transfomrationen können animiert werden aber... funktioniert das überhaupt mit scrollen? außerdem ist das problem dass ich ja auch dieses "ziehen" habe, also mousedown + mousemove bewegt den container entsprechend der Mausposition und beim loslassen erst wird der wert dann auf das nächste item fixiert. gängiges verhalten. Also selbst wenn Transform funktioniert müsste ich dann quasi mit 60FPS live updates des gesamten HTML fahren - denn es muss ja inline CSS sein damit das überhaupt funktioniert. Ist ja nicht statisch. ist das dann immer noch performant?
 
Die Sparation zwischen Scroll Events und deiner Zieh-Technik kannst du lösen indem du bei mousedown/touchstart eine Klasse setzt, die du bei mouseup/touchend wieder entfernst. In den Handlern für die Scrollevents checkst du ob die Klasse gesetzt ist und returnst die Funktion frühzeitig.
Wenn ich richtig verstehe was du vorhast würde ich für das Scrolling einen listener setzen, defaultpreventen, über die infos aus dem event feststellen in welche Richrung gescrollt wurde, die neue Scrollposition dynamisch feststellen (Position des Elements zu dem gescrollt werden soll) und dann scrollTop dorthin animaten.
 
floq0r schrieb:
Die Sparation zwischen Scroll Events und deiner Zieh-Technik kannst du lösen indem du bei mousedown/touchstart eine Klasse setzt, die du bei mouseup/touchend wieder entfernst. In den Handlern für die Scrollevents checkst du ob die Klasse gesetzt ist und returnst die Funktion frühzeitig.
Wenn ich richtig verstehe was du vorhast würde ich für das Scrolling einen listener setzen, defaultpreventen, über die infos aus dem event feststellen in welche Richrung gescrollt wurde, die neue Scrollposition dynamisch feststellen (Position des Elements zu dem gescrollt werden soll) und dann scrollTop dorthin animaten.
das meiste davon mache ich ja schon... aber moment...

das könnte tatsächlich die Lösung sein. Ich könnte meine manuellen Scrolling Mechaniken ja mit dem binden der scrollTop Property verbinden! dass ich solange der user nicht mit dem Element interagiert eine Bindung zu einem festen Wert anstrebe und sobald der user doch etwas macht kann ich die scrollanimationen anwenden.

Das einzig unsaubere daran ist, dass das scrollen ja kein promise oder so ist, das heißt ich muss ein timeout schätzen an dessen ende ich die user interaktion als beendet erkläre.

Sauberer wäre es wenn ich irgendwie wüsste wann das smooth scrolling dass ich dabei ja triggere beendet ist. gibt's da was?
 
Was meinst du mit "einer Bindung zu einem festen Wert"? Setzen der Scroll-Position ohne Animation?
Wegen der Überprüfung wann die Animation abgeschlossen ist: setInterval und checke ob die aktuelle Scrollpos der target Scrollpos entspricht, das ist aber ugly :D Vor allem dann wenn dir da was in die Quere kommt und die target Scrollpos nie erreicht wird.
Falls die Bastelei mit diesen nativen Mechaniken (Scroll) zu mühsam wird kannst dus auch mit Transformations (translateX/Y) probieren, das ist außerdem 3D-beschleunigt (bei Scroll weiß ich es nicht). Das lässt sich außerdem gut mit Transitions verbinden.
 
tja... also das mit dem fixen wert hat nicht geklappt. ich hab echt langsam das gefühl dass der browser meinen code einfach ignoriert. die property ist gesetzt aber die scroll position reagiert einfach nicht darauf. und das obwohl ich sie in die lit change detection aufgenommen habe. so funktioniert es also nicht.

das bedeutet anders ausgedrückt, dass ich erst den Wert setze, dann irgendeine Browser-Logik passiert die en wert auf was anderes setzt ohne dass ich darauf einen Einfluss habe...

Ich will aber auch kein Callback laufen lassen enn wie ud schon sagtest wäre das extrem ugly...

vielleicht hat es wirklich etwas mit dem window resize zutun, denn aktuell passiert es immer wenn ich nach rechts den inspektor öffne und ihn wieder schließe. also nicht beim öffnen, erst wenn ich ihn danach wieder schließe... versteh einer die welt...
 
Kokujou schrieb:
funktioniert das überhaupt mit scrollen?
Das Scrollen musst du abfangen und dem Browser sagen, dass er seine Aktion dazu nicht ausführen soll. Kann sein, dass du auf ein Mouse Event lauschen musst und hier das ganze stoppen, da das Scroll Event erst nach dem Scrollen gefeuert wird(?).

Kokujou schrieb:
60FPS live updates des gesamten HTML fahren
Kein Problem mit Transformationen. Schau dir dafür auch requestAnimationFrame an.

Kokujou schrieb:
ist das dann immer noch performant?
Ja. Nur Dinge, die zu einem Reflow führen kosten viel Zeit.

Kokujou schrieb:
Sauberer wäre es wenn ich irgendwie wüsste wann das smooth scrolling dass ich dabei ja triggere beendet ist. gibt's da was?
in einer Loop überprüfen, ob sich scrollTop zum vorherigen überprüfen geändert hat. Wenn nein...
Stichwort Debounce.
 
okay ich würde sagen ich habs... und jetzt kommt die Lösung, ich werde sie mal für alle die Interesse haben aufschreiben, das zieht sich aber, denn es ist komplex.

Zuerst: Man benötigt einen Parent der die größe des Scroll-Containers vorgibt. Dieser enthält einen weiteren Parent, der ein Container für die Items ist und dann natürlich noch sämtliche scroll-items. Ich arbeite da gerne mit nem flex-layout. Außerdem braucht man vor und am ender der items noch etwa 50% breite unsichtbare füller-items, damit man auch das erste und das letzte item in der Mitte der box haben kann. Ich habe mir dann noch einen Overlay ausgedacht, sowas wie ein weißer rahmen, alles außerhalb wird etwas abgedimmt. In CSS sieht das so aus. (das abdimmen mache ich ürbigens mit der CSS Maske die ihr gleich am Anfang seht)

CSS:
 #scroll-container {
            --active-item-start: calc(50% - 22px);
            --active-item-end: calc(50% + 22px);
            mask: linear-gradient(
                transparent,
                #00000055 20% var(--active-item-start),
                black var(--active-item-start) var(--active-item-end),
                #00000055 var(--active-item-end) 80%,
                transparent
            );
            max-height: 100%;
        }

#scroll-items {
            z-index: 1;
            overflow: hidden;
            scrollbar-width: none;
            height: 100%;
        }
#item-container:not(.user-interaction) {
            display: flex;
            flex-direction: column;
            transition: transform 0.5s ease-out;
        }
.inner-space {
            content: ' ';
            min-height: 50%;
            display: block;
            opacity: 0;
        }
.scroll-item {
            width: 100%;
            font-size: 18px;
            font-weight: bold;
            text-transform: uppercase;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 40px !important;
        }
#border-overlay {
            position: absolute;
            left: 0;
            right: 0;
            top: var(--active-item-start);
            height: 40px;
            border-bottom: 1px solid;
            border-top: 1px solid;
        }

jetzt der schwere Part. Der JS Part. Auf dem scroll-container liegt ein pointerdown listener, der pointermove listener liegt auf dem fenster, selbiges gilt für pointerup.

Pointer Down: Hier wird die aktuelle ScrollPosition veröffentlicht und der container mit dem "user-interaction" flag gelocked, sodass keine transition stattfindet, das würde massiv stören
Code:
startDragScrolling() {
        this.mouseDown = true;
        var element = this.scrollChildren[this.currentItemIndex];
        this.mouseStartY = getTargetScrollPosition(element, element.parentElement, this.scrollContainer).top;
        this.scrollItemsContainer.classList.toggle('user-interaction', true);
        this.requestUpdate(undefined);
}

Code:
    /**
     * @param {PointerEvent} e
     */
    onPointerMove(e) {
        if (!this.mouseDown) return;
        var deltaY = e.movementY;
        this.mouseStartY += deltaY / 2;
        this.scrollItemsContainer.style.transform = `translateY(${this.mouseStartY}px)`;
    }
leider etwas dirty, den hier wird der transform in echtzeit xmal pro sekunde modifiziert.

Code:
    onPointerUp() {
        if (!this.mouseDown) return;
        this.mouseDown = false;
        var currentScrollTop = this.mouseStartY;
        this.scrollItemsContainer.classList.toggle('user-interaction', false);
        this.scrollChildren.forEach((item, index) => {
            var targetTop = getTargetScrollPosition(item, item.parentElement, this.scrollContainer).top;
            console.log(targetTop + '|' + currentScrollTop + '|' + (targetTop + item.offsetHeight));
            if (currentScrollTop > targetTop - item.offsetHeight / 2 && currentScrollTop < targetTop + item.offsetHeight / 2) {
                this.currentItemIndex = index;
            }
        });
        this.notifyValueChanged();
    }
Am ende des Zieh-Vorgangs wird das element ermittelt, dass gerade noch am "ausgewähltesten" ist, mit einem 50% threshold.

die valueChanged dispatched dann ein CustomEvent dass von aufrufenden Komponenten gefangen werden kann und führt das scrolling an. und da kommen wir zum Mathelastigen Part:

Code:
/**
 * @param {HTMLElement} element
 * @param {HTMLElement} parent
 * @param {HTMLElement} elementContainer
 */
export function scrollIntoParentView(element, elementContainer, parent) {
    var targetLeft = element.offsetLeft + elementContainer.offsetLeft - parent.offsetWidth / 2 - element.offsetWidth / 2;
    var targetTop = element.offsetTop + elementContainer.offsetTop - parent.offsetHeight / 2 + element.offsetHeight / 2;
    elementContainer.style.transform = `translate(${targetLeft}px, ${-targetTop}px)`;
}
/**
 * @param {HTMLElement} element
 * @param {HTMLElement} parent
 * @param {HTMLElement} elementContainer
 */
export function getTargetScrollPosition(element, elementContainer, parent) {
    var targetLeft = element.offsetLeft + elementContainer.offsetLeft - parent.offsetWidth / 2 - element.offsetWidth / 2;
    var targetTop = element.offsetTop + elementContainer.offsetTop - parent.offsetHeight / 2 + element.offsetHeight / 2;
    return { left: targetLeft, top: -targetTop };
}
 
Zuletzt bearbeitet:
Zurück
Oben