Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: create component navscroll #385

Merged
merged 53 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
ebbfa66
chore: devcontainer update
valeriocomo Jul 29, 2024
1738e61
chore: setup component
valeriocomo Jul 30, 2024
ed86e1f
chore: setup doc
valeriocomo Jul 31, 2024
396a2a4
chore: postStartCommand DC
valeriocomo Jul 31, 2024
60223d2
chore: default example
valeriocomo Aug 1, 2024
9aecaa4
fix: test bed component
valeriocomo Aug 1, 2024
3b29071
chore: header
valeriocomo Aug 1, 2024
aaae5c2
chore: fix
valeriocomo Aug 2, 2024
48ee90e
chore: fix
valeriocomo Aug 2, 2024
3fe37d2
chore: ok
valeriocomo Aug 2, 2024
bc24248
feat: active menu item
valeriocomo Aug 7, 2024
4b781ec
chore: removed unused code
valeriocomo Aug 7, 2024
4ba2327
chore: fix
valeriocomo Aug 7, 2024
b08e134
chore: fix
valeriocomo Aug 7, 2024
5d44ad3
chore: setup
valeriocomo Aug 7, 2024
b71c47a
chore: fix subs
valeriocomo Aug 7, 2024
ad1c0fd
chore: init
valeriocomo Aug 7, 2024
c41714d
chore: removed setTimeout
valeriocomo Aug 8, 2024
000e503
chore: asyncsubject
valeriocomo Aug 8, 2024
e8ab67e
chore: class css
valeriocomo Aug 8, 2024
0a0babe
chore: selected title
valeriocomo Aug 8, 2024
4b3ac65
chore: theme support
valeriocomo Aug 8, 2024
7aa3c1d
fix: removed unused stuff
valeriocomo Aug 9, 2024
974e602
fix: m
valeriocomo Aug 9, 2024
fd4b351
fix: f
valeriocomo Aug 9, 2024
2388651
fix: scrolling
valeriocomo Aug 9, 2024
9cddb1d
fix: scroll
valeriocomo Aug 9, 2024
f05a7cd
fix: minor fixes
valeriocomo Aug 9, 2024
3ac4981
fix: progressbar
valeriocomo Aug 9, 2024
9cff89c
feat: optional custom template
valeriocomo Aug 26, 2024
42d380f
chore: fix
valeriocomo Aug 26, 2024
52241ac
chore: paragraph header
valeriocomo Aug 26, 2024
480ca94
fix: fix ui
valeriocomo Aug 27, 2024
cc941c9
chore: ismobile
valeriocomo Aug 27, 2024
b896abd
chore: docs
valeriocomo Aug 28, 2024
33165c8
chore: fix doc
valeriocomo Aug 28, 2024
8718042
chore: enhancement
valeriocomo Aug 30, 2024
67ef6e5
fix: animations
valeriocomo Oct 15, 2024
c2c3a45
chore: renaming
valeriocomo Oct 15, 2024
d0de2d1
fix: ref
valeriocomo Oct 15, 2024
15ef154
chore: clean codebase
valeriocomo Oct 15, 2024
f8b1d72
fix: console log removed
valeriocomo Oct 15, 2024
cb162d1
fix: lib compilation problem fixed
valeriocomo Oct 15, 2024
00c909a
fix: navscroll list item
valeriocomo Oct 15, 2024
2205866
fix: navscroll-list-item.component fixed
valeriocomo Oct 15, 2024
41a9b65
fix: compilation problems solved
valeriocomo Oct 15, 2024
9bc29a9
chore: deleted unused test
valeriocomo Oct 15, 2024
7244de9
Merge branch 'main' into 146-create-component-navscroll
valeriocomo Oct 31, 2024
d480e20
chore: examples
valeriocomo Oct 31, 2024
3bebd39
chore: refactoring and introducing examples
valeriocomo Oct 31, 2024
ac3f39b
chore: link to example
valeriocomo Oct 31, 2024
0e5f75a
chore: link to doc in example
valeriocomo Oct 31, 2024
9553afa
chore: fix
valeriocomo Oct 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .devcontainer/scripts/postStartCommand.sh
100755 → 100644
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output, ViewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
IsActiveMatchOptions,
NavigationEnd,
Router,
Event as RouterEvent,
RouterLink,
RouterLinkActive,
RouterLinkWithHref,
Scroll,
} from '@angular/router';
import { AsyncSubject, filter, switchMap, tap } from 'rxjs';
import { ItNavscrollListItemsComponent } from './navscroll-list-items.component';
import { NavscrollItem } from './navscroll.model';
import { NavscrollStore } from './navscroll.store';

const ROUTER_LINK_ACTIVE_OPTIONS: IsActiveMatchOptions = {
fragment: 'exact',
paths: 'exact',
queryParams: 'exact',
matrixParams: 'exact',
};

@Component({
selector: 'it-navscroll-list-item',
standalone: true,
imports: [RouterLink, RouterLinkActive, RouterLinkWithHref, ItNavscrollListItemsComponent, AsyncPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<a
class="nav-link"
[class.active]="active | async"
[routerLink]="[]"
routerLinkActive
[fragment]="item?.href"
[routerLinkActiveOptions]="routerLinkActiveOptions"
ariaCurrentWhenActive="page"
#rtl="routerLinkActive"
(click)="clickHandler($event)"
><span>{{ item?.title }}</span></a
>
`,
})
export class ItNavscrollListItemComponent implements OnInit {
@Input() item!: NavscrollItem;

@Output() readonly checkActive = new EventEmitter<NavscrollItem>();

@ViewChild('rtl')
readonly rtl: any;

readonly routerLinkActiveOptions = ROUTER_LINK_ACTIVE_OPTIONS;

readonly #initIsActive = new AsyncSubject<NavscrollItem>();

readonly active = this.#initIsActive.asObservable().pipe(switchMap(item => this.#store.isActive$(item)));

readonly #router = inject(Router);

readonly #store = inject(NavscrollStore);

readonly #destroyRef = inject(DestroyRef);

ngOnInit() {
this.#initIsActiveSub();
this.#router.events
.pipe(
takeUntilDestroyed(this.#destroyRef),
filter((event: RouterEvent) => {
const isNavigationEndEvent = event instanceof NavigationEnd;
const isScrollEvent = event instanceof Scroll && (event as Scroll).routerEvent instanceof NavigationEnd;
return isNavigationEndEvent || isScrollEvent;
}),
tap(() => {
if (this.rtl?.isActive) {
this.#store.setActive(this.item);
}
})
)
.subscribe();
}

clickHandler(event: Event) {
event.preventDefault();
this.#store.selectMenuItem();
this.#router.navigate([], { fragment: this.item.href });
}

#initIsActiveSub() {
this.#initIsActive.next(this.item);
this.#initIsActive.complete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { JsonPipe, NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterLinkWithHref } from '@angular/router';
import { ItNavscrollListItemComponent } from './navscroll-list-item.component';
import { NavscrollItems } from './navscroll.model';

@Component({
selector: 'it-navscroll-list-items',
standalone: true,
imports: [NgTemplateOutlet, RouterLink, RouterLinkActive, RouterLinkWithHref, JsonPipe, ItNavscrollListItemComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ul class="link-list">
@for (item of items; track item.href) {
<li class="nav-item">
<it-navscroll-list-item [item]="item"></it-navscroll-list-item>
@if (item.childs?.length) {
<it-navscroll-list-items [items]="item.childs"></it-navscroll-list-items>
}
</li>
}
</ul>
`,
})
export class ItNavscrollListItemsComponent {
@Input() items!: NavscrollItems;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<div class="container py-lg-5">
<div class="row">
<div class="col-12 col-lg-4">
<div class="it-navscroll-sticky" [ngClass]="{ 'it-navscroll-sticky-mobile': isMobile | async }" data-bs-stackable="true">
<nav
class="navbar it-navscroll-wrapper navbar-expand-lg"
[class.it-top-navscroll]="alignment === 'top'"
[class.it-bottom-navscroll]="alignment === 'bottom'"
[class.it-left-side]="borderPosition === 'left'"
[class.it-right-side]="borderPosition === 'right'"
[class.theme-dark-mobile]="theme === 'dark'"
[class.theme-dark-desktop]="theme === 'dark'">
<button
class="custom-navbar-toggler"
type="button"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
data-bs-toggle="navbarcollapsible"
data-bs-target="#navbarNav"
#toggleButtonRef>
<span class="it-list"></span>{{ selectedTitle | async }}
</button>
<div class="progress custom-navbar-progressbar">
<div
class="progress-bar it-navscroll-progressbar"
role="progressbar"
[style.width.%]="progressBarValue | async"
[attr.aria-valuenow]="progressBarValue | async"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<div class="navbar-collapsable" id="navbarNav">
<div class="overlay"></div>
<div class="close-div visually-hidden">
<button class="btn close-menu" type="button"><span class="it-close"></span>Chiudi</button>
</div>
<button type="button" class="it-back-button btn w-100 text-start">
<svg class="icon icon-sm icon-primary align-top">
<use
href="/bootstrap-italia/dist/svg/sprites.svg#it-chevron-left"
xlink:href="/bootstrap-italia/dist/svg/sprites.svg#it-chevron-left"></use>
</svg>
<span>Indietro</span>
</button>
<div class="menu-wrapper">
<div class="link-list-wrapper">
<h3>{{ header }}</h3>
<div class="progress">
<div
class="progress-bar it-navscroll-progressbar"
role="progressbar"
[style.width.%]="progressBarValue | async"
[attr.aria-valuenow]="progressBarValue | async"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<it-navscroll-list-items [items]="items"></it-navscroll-list-items>
</div>
</div>
</div>
</nav>
</div>
</div>
<div class="col-12 col-lg-8 it-page-sections-container">
<ng-container
*ngTemplateOutlet="pageSectionsTemplate ? pageSectionsTemplate : defaultPageSectionsTemplate; context: { items: items }">
</ng-container>
</div>
</div>
</div>

<ng-template #defaultPageSectionsTemplate let-items="items">
@for (item of items; track item.href) {
<ng-container *ngTemplateOutlet="paragraphTemplate; context: { item: item, level: 1 }"></ng-container>
}
</ng-template>

<ng-template #paragraphTemplate let-item="item" let-level="level" let-nextLevel="level+1">
@switch (level) {
@case (1) {
<h2 class="it-page-section" id="{{ item.href }}">{{ item.title }}</h2>
}
@case (2) {
<h3 class="it-page-section" id="{{ item.href }}">{{ item.title }}</h3>
}
@case (3) {
<h4 class="it-page-section" id="{{ item.href }}">{{ item.title }}</h4>
}
@case (4) {
<h5 class="it-page-section" id="{{ item.href }}">{{ item.title }}</h5>
}
@default {
<h6 class="it-page-section" id="{{ item.href }}">{{ item.title }}</h6>
}
}
<p>{{ item.text }}</p>
@for (item of item.childs; track item.href) {
<ng-container *ngTemplateOutlet="paragraphTemplate; context: { item: item, level: nextLevel }"></ng-container>
}
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.it-navscroll-sticky {
// data-bs-toggle="sticky"
position: sticky;
top: 0;
}

.it-navscroll-sticky-mobile {
z-index: 1020;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { AsyncPipe, NgClass, NgTemplateOutlet, ViewportScroller } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef,
HostListener,
inject,
Input,
OnInit,
TemplateRef,
ViewChild,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { RouterLink, RouterLinkActive, RouterLinkWithHref } from '@angular/router';
import { delay, filter, map, tap, withLatestFrom } from 'rxjs';
import { ItNavscrollListItemsComponent } from './navscroll-list-items.component';
import { NavscrollItem } from './navscroll.model';
import { NavscrollStore } from './navscroll.store';

/**
* Navscroll
* @description Show a list of links to anchor of the document.
*/
@Component({
selector: 'it-navscroll',
standalone: true,
imports: [
ItNavscrollListItemsComponent,
AsyncPipe,
NgTemplateOutlet,
RouterLink,
RouterLinkActive,
RouterLinkWithHref,
AsyncPipe,
NgClass,
],
templateUrl: './navscroll.component.html',
styleUrl: './navscroll.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NavscrollStore],
})
export class ItNavscrollComponent implements OnInit {
/**
* Header of the Navscroll
*/
@Input() readonly header = '';
/**
* A list of links
*/
@Input() readonly items!: Array<NavscrollItem>;
/**
* Border position
* @default left
*/
@Input() readonly borderPosition: 'left' | 'right' = 'left';
/**
* Alignment
* @default top
*/
@Input() readonly alignment: 'top' | 'bottom' = 'top';

/**
* Theme
* @default light
*/
@Input() readonly theme: 'light' | 'dark' = 'light';

/**
* Custom template for the content section
*/
@Input()
pageSectionsTemplate?: TemplateRef<any>;

@HostListener('window:scroll', ['$event']) // for window scroll events
onScroll() {
const sectionContainer = this.#elementRef.nativeElement.querySelector('.it-page-sections-container');
this.#store.updateProgressBar(sectionContainer);
}

@HostListener('window:resize', ['$event'])
onResize() {
this.#setMobile();
}

@ViewChild('toggleButtonRef')
readonly toggleButtonRef!: ElementRef<HTMLButtonElement>;

readonly #store = inject(NavscrollStore);

readonly #scroller = inject(ViewportScroller);

readonly #destroyRef = inject(DestroyRef);

readonly #elementRef = inject(ElementRef);

readonly selectedTitle = this.#store.selected.pipe(map(selected => selected?.title ?? ''));

readonly progressBarValue = this.#store.progressBar;

readonly isMobile = this.#store.isMobile;

constructor() {
this.#store.menuItemSelected
.pipe(
takeUntilDestroyed(),
withLatestFrom(this.isMobile),
tap(v => {
const isMobile = v[1];
if (isMobile) {
this.toggleButtonRef.nativeElement.click();
}
})
)
.subscribe();
}

ngOnInit(): void {
this.#initViewScrollerSubscription();
this.#store.init(this.items);
this.#setMobile();
}

#initViewScrollerSubscription() {
this.#store.selected
.pipe(
takeUntilDestroyed(this.#destroyRef),
filter(selected => Boolean(selected)),
map(v => v as NavscrollItem),
delay(0), //WA
tap({
next: ({ href }) => {
this.#scroller.scrollToAnchor(href);
},
})
)
.subscribe();
}

#setMobile() {
this.#store.setMobile(window);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface NavscrollItem {
title: string;
text: string;
href: string;
childs: NavscrollItems;
}

export type NavscrollItems = Array<NavscrollItem>;

export interface NavscrollItemActive {
active: boolean;
}
Loading
Loading