This commit is contained in:
@@ -28,7 +28,6 @@ h2 {
|
|||||||
|
|
||||||
.stack-carousel-shell {
|
.stack-carousel-shell {
|
||||||
--carousel-nav-space: 4.25rem;
|
--carousel-nav-space: 4.25rem;
|
||||||
--carousel-edge-fade-size: clamp(1.25rem, 3vw, 2.75rem);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: calc(100% + (var(--carousel-nav-space) * 2));
|
width: calc(100% + (var(--carousel-nav-space) * 2));
|
||||||
margin-inline: calc(var(--carousel-nav-space) * -1);
|
margin-inline: calc(var(--carousel-nav-space) * -1);
|
||||||
@@ -48,15 +47,12 @@ h2 {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
scroll-snap-type: none;
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(88, 166, 255, 0.45) rgba(33, 38, 45, 0.65);
|
scrollbar-color: rgba(88, 166, 255, 0.45) rgba(33, 38, 45, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stack-carousel.is-auto-scrolling {
|
|
||||||
scroll-snap-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack-slide {
|
.stack-slide {
|
||||||
flex: 0 0 min(320px, calc(100% - 0.2rem));
|
flex: 0 0 min(320px, calc(100% - 0.2rem));
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
@@ -181,7 +177,6 @@ h2 {
|
|||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.stack-carousel-shell {
|
.stack-carousel-shell {
|
||||||
--carousel-nav-space: 0;
|
--carousel-nav-space: 0;
|
||||||
--carousel-edge-fade-size: 0.6rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-inline: 0;
|
margin-inline: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,29 +6,19 @@
|
|||||||
<div
|
<div
|
||||||
class="stack-carousel-shell"
|
class="stack-carousel-shell"
|
||||||
data-disable-bg-pointer
|
data-disable-bg-pointer
|
||||||
(mouseenter)="onHoverStart()"
|
|
||||||
(mouseleave)="onHoverEnd()"
|
|
||||||
(focusin)="onFocusIn()"
|
|
||||||
(focusout)="onCarouselFocusOut($event)"
|
|
||||||
(click)="onInteractionClick()"
|
|
||||||
>
|
>
|
||||||
<button class="carousel-btn" type="button" aria-label="Previous technologies" (click)="scrollStack(-1)">
|
<button class="carousel-btn" type="button" aria-label="Previous technologies" (click)="scrollStack(-1)">
|
||||||
<span aria-hidden="true">←</span>
|
<span aria-hidden="true">←</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="stack-carousel-column" [class.has-left-fade]="showLeftFade" [class.has-right-fade]="showRightFade">
|
<div class="stack-carousel-column">
|
||||||
<div
|
<div
|
||||||
class="stack-carousel"
|
class="stack-carousel"
|
||||||
[class.is-auto-scrolling]="isAutoScrolling"
|
|
||||||
#stackCarousel
|
#stackCarousel
|
||||||
(scroll)="onCarouselScroll()"
|
|
||||||
(touchstart)="onTouchStart()"
|
|
||||||
(touchend)="onTouchEnd()"
|
|
||||||
(touchcancel)="onTouchCancel()"
|
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Tech stack carousel"
|
aria-label="Tech stack carousel"
|
||||||
>
|
>
|
||||||
@for (skill of renderedSkills; track $index) {
|
@for (skill of skills; track skill.name) {
|
||||||
<app-card cardClass="skill-card stack-slide">
|
<app-card cardClass="skill-card stack-slide">
|
||||||
<div class="skill-heading">
|
<div class="skill-heading">
|
||||||
<span class="skill-icon" role="img" [attr.aria-label]="skill.name + ' logo'">
|
<span class="skill-icon" role="img" [attr.aria-label]="skill.name + ' logo'">
|
||||||
|
|||||||
@@ -17,29 +17,8 @@ interface Skill {
|
|||||||
})
|
})
|
||||||
export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
||||||
@ViewChild('stackCarousel') private readonly stackCarousel?: ElementRef<HTMLElement>;
|
@ViewChild('stackCarousel') private readonly stackCarousel?: ElementRef<HTMLElement>;
|
||||||
private autoScrollFrameId: number | null = null;
|
private autoScrollIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
private startAutoScrollTimeout: ReturnType<typeof setTimeout> | null = null;
|
private readonly autoScrollIntervalMs = 5000;
|
||||||
private delayedResumeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private delayedHoverPauseTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private delayedResumeAt = 0;
|
|
||||||
private isHoveringCarousel = false;
|
|
||||||
private lastAutoScrollTime = 0;
|
|
||||||
private virtualScrollLeft = 0;
|
|
||||||
private currentAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
private targetAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
private isPausePendingStop = false;
|
|
||||||
private isTouchInteracting = false;
|
|
||||||
private isProgrammaticScrollUpdate = false;
|
|
||||||
private lastFadeStateUpdateTime = 0;
|
|
||||||
private readonly autoScrollSpeedPxPerSecond = 48;
|
|
||||||
private readonly speedRampDurationMs = 420;
|
|
||||||
private readonly speedStopThresholdPxPerSecond = 0.5;
|
|
||||||
private readonly touchScrollResumeDelayMs = 1200;
|
|
||||||
private readonly hoverPauseDelayMs = 300;
|
|
||||||
private readonly fadeUpdateIntervalMs = 96;
|
|
||||||
isAutoScrolling = false;
|
|
||||||
showLeftFade = false;
|
|
||||||
showRightFade = false;
|
|
||||||
|
|
||||||
readonly skills: Skill[] = [
|
readonly skills: Skill[] = [
|
||||||
{ name: 'Angular', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/angular.svg', fallbackLabel: 'NG' },
|
{ name: 'Angular', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/angular.svg', fallbackLabel: 'NG' },
|
||||||
@@ -56,38 +35,12 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
|||||||
{ name: 'Flutter', category: 'Mobile', level: 'Advanced', logoUrl: '/tech-logos/flutter.svg', fallbackLabel: 'FL' },
|
{ name: 'Flutter', category: 'Mobile', level: 'Advanced', logoUrl: '/tech-logos/flutter.svg', fallbackLabel: 'FL' },
|
||||||
];
|
];
|
||||||
|
|
||||||
readonly renderedSkills: Skill[] = [...this.skills, ...this.skills];
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
const prefersReducedMotion =
|
this.startAutoScroll();
|
||||||
typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
||||||
|
|
||||||
if (prefersReducedMotion) {
|
|
||||||
this.updateFadeState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer start to the next task to avoid NG0100 in dev mode.
|
|
||||||
this.startAutoScrollTimeout = setTimeout(() => {
|
|
||||||
this.startAutoScrollTimeout = null;
|
|
||||||
this.updateFadeState();
|
|
||||||
this.resumeAutoScroll();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', this.onVisibilityChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.startAutoScrollTimeout !== null) {
|
this.stopAutoScroll();
|
||||||
clearTimeout(this.startAutoScrollTimeout);
|
|
||||||
this.startAutoScrollTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearScheduledResume();
|
|
||||||
this.clearScheduledHoverPause();
|
|
||||||
this.pauseAutoScroll();
|
|
||||||
document.removeEventListener('visibilitychange', this.onVisibilityChange);
|
|
||||||
this.isAutoScrolling = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollStack(direction: -1 | 1): void {
|
scrollStack(direction: -1 | 1): void {
|
||||||
@@ -96,157 +49,17 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxScrollLeft = Math.max(carousel.scrollWidth - carousel.clientWidth, 0);
|
||||||
|
if (maxScrollLeft <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === 1 && carousel.scrollLeft >= maxScrollLeft) {
|
||||||
|
carousel.scrollTo({ left: 0, behavior: 'auto' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
carousel.scrollBy({ left: this.getScrollStep(carousel) * direction, behavior: 'smooth' });
|
carousel.scrollBy({ left: this.getScrollStep(carousel) * direction, behavior: 'smooth' });
|
||||||
requestAnimationFrame(() => this.updateFadeState(carousel));
|
|
||||||
}
|
|
||||||
|
|
||||||
onCarouselScroll(): void {
|
|
||||||
if (this.isProgrammaticScrollUpdate) {
|
|
||||||
this.isProgrammaticScrollUpdate = false;
|
|
||||||
this.updateFadeState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep autoscroll from fighting touch/momentum scrolling on mobile.
|
|
||||||
this.pauseAutoScrollImmediately();
|
|
||||||
this.scheduleResume(this.touchScrollResumeDelayMs);
|
|
||||||
this.updateFadeState();
|
|
||||||
}
|
|
||||||
|
|
||||||
onTouchStart(): void {
|
|
||||||
this.isTouchInteracting = true;
|
|
||||||
this.clearScheduledResume();
|
|
||||||
this.pauseAutoScrollImmediately();
|
|
||||||
}
|
|
||||||
|
|
||||||
onTouchEnd(): void {
|
|
||||||
this.isTouchInteracting = false;
|
|
||||||
this.scheduleResume(this.touchScrollResumeDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
onTouchCancel(): void {
|
|
||||||
this.isTouchInteracting = false;
|
|
||||||
this.scheduleResume(this.touchScrollResumeDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
onHoverStart(): void {
|
|
||||||
this.isHoveringCarousel = true;
|
|
||||||
this.clearScheduledResume();
|
|
||||||
this.clearScheduledHoverPause();
|
|
||||||
|
|
||||||
this.delayedHoverPauseTimeout = setTimeout(() => {
|
|
||||||
this.delayedHoverPauseTimeout = null;
|
|
||||||
|
|
||||||
if (!this.isHoveringCarousel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pauseAutoScroll();
|
|
||||||
}, this.hoverPauseDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
onHoverEnd(): void {
|
|
||||||
this.isHoveringCarousel = false;
|
|
||||||
this.clearScheduledHoverPause();
|
|
||||||
this.scheduleResume(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
onFocusIn(): void {
|
|
||||||
this.pauseAutoScroll();
|
|
||||||
this.clearScheduledResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
onInteractionClick(): void {
|
|
||||||
this.pauseAutoScroll();
|
|
||||||
this.scheduleResume(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
pauseAutoScroll(): void {
|
|
||||||
this.targetAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
this.isPausePendingStop = true;
|
|
||||||
this.syncAutoScrollState();
|
|
||||||
|
|
||||||
if (this.autoScrollFrameId === null) {
|
|
||||||
this.currentAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
this.lastAutoScrollTime = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeAutoScroll(): void {
|
|
||||||
if (document.hidden) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isTouchInteracting) {
|
|
||||||
this.scheduleResume(this.touchScrollResumeDelayMs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isPausePendingStop = false;
|
|
||||||
this.targetAutoScrollSpeedPxPerSecond = this.autoScrollSpeedPxPerSecond;
|
|
||||||
this.syncAutoScrollState();
|
|
||||||
|
|
||||||
if (this.autoScrollFrameId !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const step = (timestamp: number) => {
|
|
||||||
const carousel = this.stackCarousel?.nativeElement;
|
|
||||||
if (!carousel) {
|
|
||||||
this.autoScrollFrameId = null;
|
|
||||||
this.lastAutoScrollTime = 0;
|
|
||||||
this.virtualScrollLeft = 0;
|
|
||||||
this.currentAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
this.targetAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
this.isPausePendingStop = false;
|
|
||||||
this.isAutoScrolling = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.lastAutoScrollTime === 0) {
|
|
||||||
const loopWidth = this.getLoopWidth(carousel);
|
|
||||||
this.virtualScrollLeft = loopWidth > 0 ? carousel.scrollLeft : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deltaMs = this.lastAutoScrollTime > 0 ? Math.min(timestamp - this.lastAutoScrollTime, 40) : 16;
|
|
||||||
this.lastAutoScrollTime = timestamp;
|
|
||||||
|
|
||||||
this.updateAutoScrollSpeed(deltaMs);
|
|
||||||
|
|
||||||
if (this.currentAutoScrollSpeedPxPerSecond > this.speedStopThresholdPxPerSecond) {
|
|
||||||
this.autoAdvanceStack(carousel, deltaMs, this.currentAutoScrollSpeedPxPerSecond);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamp - this.lastFadeStateUpdateTime >= this.fadeUpdateIntervalMs) {
|
|
||||||
this.updateFadeState(carousel);
|
|
||||||
this.lastFadeStateUpdateTime = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncAutoScrollState();
|
|
||||||
|
|
||||||
if (this.isPausePendingStop && this.currentAutoScrollSpeedPxPerSecond <= this.speedStopThresholdPxPerSecond) {
|
|
||||||
this.currentAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
this.targetAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
this.lastAutoScrollTime = 0;
|
|
||||||
this.autoScrollFrameId = null;
|
|
||||||
this.isPausePendingStop = false;
|
|
||||||
this.syncAutoScrollState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.autoScrollFrameId = requestAnimationFrame(step);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.autoScrollFrameId = requestAnimationFrame(step);
|
|
||||||
}
|
|
||||||
|
|
||||||
onCarouselFocusOut(event: FocusEvent): void {
|
|
||||||
const next = event.relatedTarget as Node | null;
|
|
||||||
const shell = event.currentTarget as HTMLElement | null;
|
|
||||||
|
|
||||||
if (!shell?.contains(next)) {
|
|
||||||
this.scheduleResume(1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLogoError(event: Event): void {
|
onLogoError(event: Event): void {
|
||||||
@@ -255,128 +68,27 @@ export class TechStackCarousel implements AfterViewInit, OnDestroy {
|
|||||||
wrapper?.classList.add('is-fallback');
|
wrapper?.classList.add('is-fallback');
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleResume(delayMs: number): void {
|
|
||||||
const nextResumeAt = Date.now() + delayMs;
|
|
||||||
if (nextResumeAt <= this.delayedResumeAt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.delayedResumeAt = nextResumeAt;
|
|
||||||
|
|
||||||
if (this.delayedResumeTimeout !== null) {
|
|
||||||
clearTimeout(this.delayedResumeTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.delayedResumeTimeout = setTimeout(() => {
|
|
||||||
this.delayedResumeTimeout = null;
|
|
||||||
this.delayedResumeAt = 0;
|
|
||||||
this.resumeAutoScroll();
|
|
||||||
}, delayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearScheduledResume(): void {
|
|
||||||
if (this.delayedResumeTimeout !== null) {
|
|
||||||
clearTimeout(this.delayedResumeTimeout);
|
|
||||||
this.delayedResumeTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.delayedResumeAt = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearScheduledHoverPause(): void {
|
|
||||||
if (this.delayedHoverPauseTimeout !== null) {
|
|
||||||
clearTimeout(this.delayedHoverPauseTimeout);
|
|
||||||
this.delayedHoverPauseTimeout = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateAutoScrollSpeed(deltaMs: number): void {
|
|
||||||
if (deltaMs <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const speedDelta = this.targetAutoScrollSpeedPxPerSecond - this.currentAutoScrollSpeedPxPerSecond;
|
|
||||||
if (Math.abs(speedDelta) <= this.speedStopThresholdPxPerSecond) {
|
|
||||||
this.currentAutoScrollSpeedPxPerSecond = this.targetAutoScrollSpeedPxPerSecond;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxStep = (this.autoScrollSpeedPxPerSecond * deltaMs) / this.speedRampDurationMs;
|
|
||||||
const appliedStep = Math.sign(speedDelta) * Math.min(Math.abs(speedDelta), maxStep);
|
|
||||||
this.currentAutoScrollSpeedPxPerSecond += appliedStep;
|
|
||||||
}
|
|
||||||
|
|
||||||
private autoAdvanceStack(carousel: HTMLElement, deltaMs: number, speedPxPerSecond: number): void {
|
|
||||||
const loopWidth = this.getLoopWidth(carousel);
|
|
||||||
if (loopWidth <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextVirtualScrollLeft = this.virtualScrollLeft + (speedPxPerSecond * deltaMs) / 1000;
|
|
||||||
|
|
||||||
// Normalize only when moving past the duplicated track boundary to avoid abrupt remaps on quick re-entry.
|
|
||||||
this.virtualScrollLeft = nextVirtualScrollLeft >= loopWidth ? nextVirtualScrollLeft - loopWidth : nextVirtualScrollLeft;
|
|
||||||
|
|
||||||
this.isProgrammaticScrollUpdate = true;
|
|
||||||
carousel.scrollLeft = this.virtualScrollLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLoopWidth(carousel: HTMLElement): number {
|
|
||||||
return carousel.scrollWidth / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateFadeState(carousel = this.stackCarousel?.nativeElement): void {
|
|
||||||
if (!carousel) {
|
|
||||||
this.showLeftFade = false;
|
|
||||||
this.showRightFade = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxScroll = Math.max(carousel.scrollWidth - carousel.clientWidth, 0);
|
|
||||||
if (maxScroll <= 1) {
|
|
||||||
this.showLeftFade = false;
|
|
||||||
this.showRightFade = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const left = carousel.scrollLeft;
|
|
||||||
this.showLeftFade = left > 1;
|
|
||||||
this.showRightFade = left < maxScroll - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getScrollStep(carousel: HTMLElement): number {
|
private getScrollStep(carousel: HTMLElement): number {
|
||||||
return Math.max(carousel.clientWidth * 0.82, 260);
|
return Math.max(carousel.clientWidth * 0.82, 260);
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncAutoScrollState(): void {
|
private startAutoScroll(): void {
|
||||||
this.isAutoScrolling =
|
if (this.autoScrollIntervalId !== null) {
|
||||||
this.targetAutoScrollSpeedPxPerSecond > this.speedStopThresholdPxPerSecond ||
|
|
||||||
this.currentAutoScrollSpeedPxPerSecond > this.speedStopThresholdPxPerSecond;
|
|
||||||
}
|
|
||||||
|
|
||||||
private pauseAutoScrollImmediately(): void {
|
|
||||||
this.clearScheduledResume();
|
|
||||||
|
|
||||||
if (this.autoScrollFrameId !== null) {
|
|
||||||
cancelAnimationFrame(this.autoScrollFrameId);
|
|
||||||
this.autoScrollFrameId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isPausePendingStop = false;
|
|
||||||
this.currentAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
this.targetAutoScrollSpeedPxPerSecond = 0;
|
|
||||||
this.lastAutoScrollTime = 0;
|
|
||||||
this.lastFadeStateUpdateTime = 0;
|
|
||||||
this.syncAutoScrollState();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly onVisibilityChange = (): void => {
|
|
||||||
if (document.hidden) {
|
|
||||||
this.pauseAutoScrollImmediately();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scheduleResume(320);
|
this.autoScrollIntervalId = setInterval(() => {
|
||||||
};
|
this.scrollStack(1);
|
||||||
|
}, this.autoScrollIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopAutoScroll(): void {
|
||||||
|
if (this.autoScrollIntervalId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(this.autoScrollIntervalId);
|
||||||
|
this.autoScrollIntervalId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user