mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 00:52:55 +00:00
feat: section dynamic slide buttons
This commit is contained in:
@@ -52,9 +52,9 @@ export class IdCardsProvider {
|
||||
map(svg => {
|
||||
let result = svg;
|
||||
for (const key in user) {
|
||||
result = result.replaceAll(`{{${key}}`, (user as unknown as Record<string, string>)[key]);
|
||||
result = result.replaceAll(`{{${key}}}`, (user as unknown as Record<string, string>)[key]);
|
||||
}
|
||||
return `data:image/svg+xml;base64,${Buffer.from(result, 'base64').toString('base64')}`;
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(result)}`;
|
||||
}),
|
||||
map(image => [
|
||||
{
|
||||
|
||||
22
frontend/app/src/app/util/rxjs/from-intersection-observer.ts
Normal file
22
frontend/app/src/app/util/rxjs/from-intersection-observer.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {Observable, shareReplay} from 'rxjs';
|
||||
|
||||
/**
|
||||
* Create an observable from an intersection observer
|
||||
*/
|
||||
export function fromIntersectionObserver(
|
||||
target: Element,
|
||||
init?: IntersectionObserverInit,
|
||||
): Observable<IntersectionObserverEntry[]> {
|
||||
return new Observable<IntersectionObserverEntry[]>(observer => {
|
||||
const intersectionObserver = new IntersectionObserver(item => {
|
||||
observer.next(item);
|
||||
}, init);
|
||||
intersectionObserver.observe(target);
|
||||
|
||||
return {
|
||||
unsubscribe() {
|
||||
intersectionObserver.disconnect();
|
||||
},
|
||||
};
|
||||
}).pipe(shareReplay(1));
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Observable} from 'rxjs';
|
||||
import {Observable, shareReplay} from 'rxjs';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -22,7 +22,7 @@ export function fromMutationObserver(
|
||||
target: Node,
|
||||
options?: MutationObserverInit,
|
||||
): Observable<MutationRecord[]> {
|
||||
return new Observable(subscriber => {
|
||||
return new Observable<MutationRecord[]>(subscriber => {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
subscriber.next(mutations);
|
||||
});
|
||||
@@ -30,5 +30,5 @@ export function fromMutationObserver(
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
}).pipe(shareReplay(1));
|
||||
}
|
||||
@@ -14,17 +14,33 @@
|
||||
*/
|
||||
import {AfterContentInit, ChangeDetectionStrategy, Component, Input, ViewContainerRef} from '@angular/core';
|
||||
import {SCThings} from '@openstapps/core';
|
||||
import {fromMutationObserver} from '../_helpers/rxjs/mutation-observer';
|
||||
import {mergeMap, ReplaySubject, takeLast} from 'rxjs';
|
||||
import {fromMutationObserver} from './rxjs/mutation-observer';
|
||||
import {combineLatestWith, mergeMap, OperatorFunction, ReplaySubject, takeLast} from 'rxjs';
|
||||
import {distinctUntilChanged, filter, map, startWith} from 'rxjs/operators';
|
||||
import {fromIntersectionObserver} from './rxjs/from-intersection-observer';
|
||||
|
||||
/**
|
||||
* Operator function that checks if a slide is visible
|
||||
*/
|
||||
function isSlideVisible(
|
||||
select: (slides: HTMLCollection) => Element | null,
|
||||
): OperatorFunction<readonly [HTMLElement, HTMLCollection], boolean> {
|
||||
return source =>
|
||||
source.pipe(
|
||||
map(([element, slides]) => [element, select(slides) as HTMLElement]),
|
||||
filter(([, slide]) => slide !== null),
|
||||
mergeMap(([element, slide]) => fromIntersectionObserver(slide, {threshold: 1, root: element})),
|
||||
map(entry => entry.some(it => it.intersectionRatio === 1)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a horizontal list of action chips
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-section',
|
||||
templateUrl: 'section.component.html',
|
||||
styleUrls: ['section.component.scss'],
|
||||
templateUrl: 'section.html',
|
||||
styleUrls: ['section.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SectionComponent implements AfterContentInit {
|
||||
@@ -36,6 +52,9 @@ export class SectionComponent implements AfterContentInit {
|
||||
|
||||
nativeElement = new ReplaySubject<HTMLElement>(1);
|
||||
|
||||
/**
|
||||
* The swiper child (may not emit at all)
|
||||
*/
|
||||
swiper = this.nativeElement.pipe(
|
||||
takeLast(1),
|
||||
mergeMap(element =>
|
||||
@@ -51,6 +70,44 @@ export class SectionComponent implements AfterContentInit {
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Emits the current list of all slides
|
||||
*/
|
||||
slides = this.swiper.pipe(
|
||||
mergeMap(element =>
|
||||
fromMutationObserver(element, {
|
||||
childList: true,
|
||||
}).pipe(
|
||||
map(() => element.children),
|
||||
startWith(element.children),
|
||||
map(it => [element, it] as const),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Emits true when the first slide is fully visible,
|
||||
* false when it's only partially or not visible
|
||||
*/
|
||||
firstSlideVisible = this.slides.pipe(isSlideVisible(slides => slides.item(0)));
|
||||
|
||||
/**
|
||||
* Emits true when the last slide is fully visible,
|
||||
* false when it's only partially or not visible
|
||||
*/
|
||||
lastSlideVisible = this.slides.pipe(isSlideVisible(slides => slides.item(slides.length - 1)));
|
||||
|
||||
/**
|
||||
* If the nav should be shown
|
||||
*
|
||||
* Emits false if all slides are visible
|
||||
*/
|
||||
showNav = this.slides.pipe(
|
||||
map(([, slides]) => slides.length > 1),
|
||||
combineLatestWith(this.firstSlideVisible, this.lastSlideVisible),
|
||||
map(([multipleSlides, firstVisible, lastVisible]) => multipleSlides && !(firstVisible && lastVisible)),
|
||||
);
|
||||
|
||||
constructor(readonly viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
ngAfterContentInit() {
|
||||
|
||||
@@ -25,27 +25,29 @@
|
||||
</ng-template>
|
||||
</ion-col>
|
||||
|
||||
<ng-container *ngIf="swiper | async as swiper">
|
||||
<ion-col size="auto" class="swiper-button">
|
||||
<ion-button
|
||||
fill="clear"
|
||||
[color]="buttonColor"
|
||||
(click)="swiper.scrollBy({left: -swiper.offsetWidth, behavior: 'smooth'})"
|
||||
[disabled]="false"
|
||||
>
|
||||
<ion-icon size="24" slot="icon-only" name="chevron_left"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col size="auto" class="swiper-button">
|
||||
<ion-button
|
||||
fill="clear"
|
||||
[color]="buttonColor"
|
||||
(click)="swiper.scrollBy({left: swiper.offsetWidth, behavior: 'smooth'})"
|
||||
[disabled]="false"
|
||||
>
|
||||
<ion-icon size="24" slot="icon-only" name="chevron_right"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ng-container *ngIf="showNav | async">
|
||||
<ng-container *ngIf="swiper | async as swiper">
|
||||
<ion-col size="auto" class="swiper-button">
|
||||
<ion-button
|
||||
fill="clear"
|
||||
[color]="buttonColor"
|
||||
(click)="swiper.scrollBy({left: -swiper.offsetWidth, behavior: 'smooth'})"
|
||||
[disabled]="firstSlideVisible | async"
|
||||
>
|
||||
<ion-icon size="24" slot="icon-only" name="chevron_left"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col size="auto" class="swiper-button">
|
||||
<ion-button
|
||||
fill="clear"
|
||||
[color]="buttonColor"
|
||||
(click)="swiper.scrollBy({left: swiper.offsetWidth, behavior: 'smooth'})"
|
||||
[disabled]="lastSlideVisible | async"
|
||||
>
|
||||
<ion-icon size="24" slot="icon-only" name="chevron_right"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ion-col size="auto">
|
||||
<div>
|
||||
Reference in New Issue
Block a user