fix: parallax broken since safari 16.4

Moves the parallax effect into a directive which injects required elements into the shadow DOM and adds `part` attributes, so it can be styled from a global stylesheet.
This commit is contained in:
Thea Schöbl
2023-05-04 12:18:48 +00:00
parent abf2ab6a5a
commit 0f970fa1f1
16 changed files with 428 additions and 358 deletions

View File

@@ -22,10 +22,8 @@
<!-- TODO: translation --> <!-- TODO: translation -->
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-content-parallax"> <ion-content parallax>
<div> <div class="about-changelog">
<div class="about-changelog"> <markdown src="assets/about/CHANGELOG.md"></markdown>
<markdown src="assets/about/CHANGELOG.md"></markdown>
</div>
</div> </div>
</ion-content> </ion-content>

View File

@@ -22,32 +22,30 @@
<!-- TODO: translation --> <!-- TODO: translation -->
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-content-parallax"> <ion-content parallax>
<div> <div class="licenses-content">
<div class="licenses-content"> <ion-card
<ion-card *ngFor="let license of licenses"
*ngFor="let license of licenses" [href]="license.url || license.repository"
[href]="license.url || license.repository" rel="external"
rel="external" target="_blank"
target="_blank" >
> <ion-card-header>
<ion-card-header> <ion-card-title>
<ion-card-title> {{ license.name }}
{{ license.name }} <ion-icon size="16" weight="300" class="supertext-icon" name="open_in_browser"></ion-icon>
<ion-icon size="16" weight="300" class="supertext-icon" name="open_in_browser"></ion-icon> </ion-card-title>
</ion-card-title>
<ion-card-subtitle *ngIf="license.authors || license.publisher"> <ion-card-subtitle *ngIf="license.authors || license.publisher">
{{ license.authors || license.publisher }} {{ license.authors || license.publisher }}
</ion-card-subtitle> </ion-card-subtitle>
</ion-card-header> </ion-card-header>
<ion-card-content> <ion-card-content>
<ion-chip (click)="$event.preventDefault(); viewLicense(license)"> <ion-chip (click)="$event.preventDefault(); viewLicense(license)">
<ion-icon name="copyright"></ion-icon> <ion-icon name="copyright"></ion-icon>
<ion-label>{{ license.licenses }} License</ion-label> <ion-label>{{ license.licenses }} License</ion-label>
</ion-chip> </ion-chip>
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>
</div>
</div> </div>
</ion-content> </ion-content>

View File

@@ -24,11 +24,9 @@
</ng-template> </ng-template>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content *ngIf="content" class="ion-content-parallax"> <ion-content parallax *ngIf="content">
<div> <ion-text>{{ appName }} v{{ version }}</ion-text>
<ion-text>{{ appName }} v{{ version }}</ion-text> <div class="page-content">
<div class="page-content"> <about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
<about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
</div>
</div> </div>
</ion-content> </ion-content>

View File

@@ -1,5 +1,5 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
@@ -26,14 +26,12 @@
<ion-label>{{ label | translate }}</ion-label> <ion-label>{{ label | translate }}</ion-label>
<stapps-edit-modal #editModal (save)="selection.save()"> <stapps-edit-modal #editModal (save)="selection.save()">
<ng-template> <ng-template>
<ion-content class="ion-padding modal-content"> <ion-content parallax [parallaxSize]="160" class="ion-padding modal-content">
<div> <stapps-edit-event-selection
<stapps-edit-event-selection #selection
#selection [items]="associatedDateSeries"
[items]="associatedDateSeries" (modified)="editModal.pendingChanges = true"
(modified)="editModal.pendingChanges = true" ></stapps-edit-event-selection>
></stapps-edit-event-selection>
</div>
</ion-content> </ion-content>
<ion-footer mode="ios"> <ion-footer mode="ios">
<ion-toolbar color="light"> <ion-toolbar color="light">

View File

@@ -12,8 +12,6 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@import 'src/theme/common/ion-content-parallax';
:host { :host {
display: block; display: block;
padding: var(--spacing-sm); padding: var(--spacing-sm);
@@ -35,10 +33,7 @@
} }
.modal-content { .modal-content {
--background: var(--ion-color-primary);
--color: var(--ion-color-primary-contrast); --color: var(--ion-color-primary-contrast);
@include ion-content-parallax($content-size: 160px);
} }
ion-footer > ion-toolbar { ion-footer > ion-toolbar {

View File

@@ -1,5 +1,5 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
@@ -30,8 +30,8 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ng-content select="[header]"></ng-content> <ng-content select="[header]"></ng-content>
<ion-content class="ion-no-padding ion-content-parallax"> <ion-content parallax class="ion-no-padding">
<div [ngSwitch]="true"> <ng-container [ngSwitch]="true">
<ng-container *ngSwitchCase="!item && (isDisconnected | async)"> <ng-container *ngSwitchCase="!item && (isDisconnected | async)">
<div class="centeredMessageContainer"> <div class="centeredMessageContainer">
<ion-icon name="signal_disconnected"></ion-icon> <ion-icon name="signal_disconnected"></ion-icon>
@@ -59,5 +59,5 @@
[contentTemplateRef]="contentTemplateRef" [contentTemplateRef]="contentTemplateRef"
></stapps-data-detail-content> ></stapps-data-detail-content>
</ng-container> </ng-container>
</div> </ng-container>
</ion-content> </ion-content>

View File

@@ -12,11 +12,6 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
:host {
z-index: 100;
}
.crumb-label { .crumb-label {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;

View File

@@ -21,107 +21,105 @@
<ion-title>{{ 'feedback.page.TITLE' | translate }}</ion-title> <ion-title>{{ 'feedback.page.TITLE' | translate }}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-content-parallax"> <ion-content parallax>
<div> <div class="feedback-content">
<div class="feedback-content"> <ion-card>
<ion-card> <form #feedbackForm="ngForm" (ngSubmit)="onSubmit()">
<form #feedbackForm="ngForm" (ngSubmit)="onSubmit()"> <ion-item>
<ion-item> <ion-label position="stacked">{{ 'feedback.form.name.label' | translate }}</ion-label>
<ion-label position="stacked">{{ 'feedback.form.name.label' | translate }}</ion-label> <ion-input
<ion-input placeholder="{{ 'feedback.form.name.placeholder' | translate }}"
placeholder="{{ 'feedback.form.name.placeholder' | translate }}" [(ngModel)]="author.name"
[(ngModel)]="author.name" name="name"
name="name" ></ion-input>
></ion-input> </ion-item>
</ion-item> <ion-item>
<ion-item> <ion-label position="stacked">{{ 'feedback.form.type.label' | translate }}</ion-label>
<ion-label position="stacked">{{ 'feedback.form.type.label' | translate }}</ion-label> <ion-select
<ion-select [(ngModel)]="message.name"
[(ngModel)]="message.name" value="comment"
value="comment" name="title"
name="title" interface="popover"
interface="popover" required="true"
required="true"
>
<ion-select-option value="Comment">{{
'feedback.form.type.values.comment' | translate
}}</ion-select-option>
<ion-select-option value="Bug">{{
'feedback.form.type.values.bug' | translate
}}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'feedback.form.email.label' | translate }}</ion-label>
<ion-input
placeholder="{{ 'feedback.form.email.placeholder' | translate }}"
[(ngModel)]="author.email"
type="email"
name="email"
ngModel
email
></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'feedback.form.message.label' | translate }}</ion-label>
<ion-textarea
[(ngModel)]="message.messageBody"
placeholder="{{
'feedback.form.message.placeholder' | translate: {number: MINIMUM_MESSAGE_SIZE}
}}"
name="message"
required="true"
minlength="{{ MINIMUM_MESSAGE_SIZE }}"
autoGrow="true"
></ion-textarea>
</ion-item>
<ion-item>
<ion-label class="ion-text-wrap">{{ 'feedback.form.termsAgree.0' | translate }} </ion-label>
<ion-checkbox
color="primary"
slot="start"
[(ngModel)]="termsAgree"
name="termsAgree"
></ion-checkbox>
</ion-item>
<ion-item lines="none">
<ion-label
><a style="display: contents" [routerLink]="['/about/privacy']">{{
'feedback.form.termsAgree.1' | translate
}}</a></ion-label
>
</ion-item>
<ion-item>
<ion-label class="ion-text-wrap">{{ 'feedback.form.protocolDataAgree' | translate }}</ion-label>
<ion-checkbox
color="primary"
slot="start"
[(ngModel)]="protocolDataAgree"
name="protocolDataAgree"
></ion-checkbox>
</ion-item>
<ion-card>
<ion-card-title>
<ion-button expand="block" fill="clear" (click)="toggleShowMetaData()">
<ng-container *ngIf="!showMetaData; else hide">{{
'feedback.form.protocolData.show' | translate
}}</ng-container>
<ng-template #hide>{{ 'feedback.form.protocolData.hide' | translate }}</ng-template>
</ion-button>
</ion-card-title>
<ion-card-content *ngIf="metaData && showMetaData">
<pre>{{ metaData | json }}</pre>
</ion-card-content>
</ion-card>
<ion-button
type="submit"
color="primary"
expand="block"
[disabled]="!feedbackForm.valid || !termsAgree || submitSuccess"
>{{ 'feedback.form.submit' | translate }}</ion-button
> >
</form> <ion-select-option value="Comment">{{
</ion-card> 'feedback.form.type.values.comment' | translate
</div> }}</ion-select-option>
<ion-select-option value="Bug">{{
'feedback.form.type.values.bug' | translate
}}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'feedback.form.email.label' | translate }}</ion-label>
<ion-input
placeholder="{{ 'feedback.form.email.placeholder' | translate }}"
[(ngModel)]="author.email"
type="email"
name="email"
ngModel
email
></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'feedback.form.message.label' | translate }}</ion-label>
<ion-textarea
[(ngModel)]="message.messageBody"
placeholder="{{
'feedback.form.message.placeholder' | translate: {number: MINIMUM_MESSAGE_SIZE}
}}"
name="message"
required="true"
minlength="{{ MINIMUM_MESSAGE_SIZE }}"
autoGrow="true"
></ion-textarea>
</ion-item>
<ion-item>
<ion-label class="ion-text-wrap">{{ 'feedback.form.termsAgree.0' | translate }} </ion-label>
<ion-checkbox
color="primary"
slot="start"
[(ngModel)]="termsAgree"
name="termsAgree"
></ion-checkbox>
</ion-item>
<ion-item lines="none">
<ion-label
><a style="display: contents" [routerLink]="['/about/privacy']">{{
'feedback.form.termsAgree.1' | translate
}}</a></ion-label
>
</ion-item>
<ion-item>
<ion-label class="ion-text-wrap">{{ 'feedback.form.protocolDataAgree' | translate }}</ion-label>
<ion-checkbox
color="primary"
slot="start"
[(ngModel)]="protocolDataAgree"
name="protocolDataAgree"
></ion-checkbox>
</ion-item>
<ion-card>
<ion-card-title>
<ion-button expand="block" fill="clear" (click)="toggleShowMetaData()">
<ng-container *ngIf="!showMetaData; else hide">{{
'feedback.form.protocolData.show' | translate
}}</ng-container>
<ng-template #hide>{{ 'feedback.form.protocolData.hide' | translate }}</ng-template>
</ion-button>
</ion-card-title>
<ion-card-content *ngIf="metaData && showMetaData">
<pre>{{ metaData | json }}</pre>
</ion-card-content>
</ion-card>
<ion-button
type="submit"
color="primary"
expand="block"
[disabled]="!feedbackForm.valid || !termsAgree || submitSuccess"
>{{ 'feedback.form.submit' | translate }}</ion-button
>
</form>
</ion-card>
</div> </div>
</ion-content> </ion-content>

View File

@@ -22,41 +22,39 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-content-parallax" (elementSizeChange)="calcPageSize($event)"> <ion-content parallax (elementSizeChange)="calcPageSize($event)">
<div> <ion-refresher slot="fixed" (ionRefresh)="refresh($event.target)">
<ion-refresher slot="fixed" (ionRefresh)="refresh($event.target)"> <ion-refresher-content
<ion-refresher-content pullingIcon="chevron-down-outline"
pullingIcon="chevron-down-outline" pullingText="{{ 'data.REFRESH_ACTION' | translate }}"
pullingText="{{ 'data.REFRESH_ACTION' | translate }}" refreshingText="{{ 'data.REFRESHING' | translate }}"
refreshingText="{{ 'data.REFRESHING' | translate }}" refreshingSpinner="crescent"
refreshingSpinner="crescent" >
> </ion-refresher-content>
</ion-refresher-content> </ion-refresher>
</ion-refresher> <ion-grid>
<ion-grid> <ion-row>
<ion-row> <ion-col size="12">
<ion-col size="12"> <stapps-news-settings-filter
<stapps-news-settings-filter *ngIf="settings"
*ngIf="settings" [settings]="settings"
[settings]="settings" (filtersChanged)="toggleFilter($event)"
(filtersChanged)="toggleFilter($event)" ></stapps-news-settings-filter>
></stapps-news-settings-filter> </ion-col>
</ion-col> </ion-row>
</ion-row> </ion-grid>
</ion-grid> <div class="news-grid">
<div class="news-grid"> <ng-container *ngIf="!news">
<ng-container *ngIf="!news"> <stapps-skeleton-news-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-news-item>
<stapps-skeleton-news-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-news-item> </ng-container>
</ng-container> <ng-container *ngIf="news.length > 0">
<ng-container *ngIf="news.length > 0"> <stapps-news-item *ngFor="let item of news" [item]="item"></stapps-news-item>
<stapps-news-item *ngFor="let item of news" [item]="item"></stapps-news-item> </ng-container>
</ng-container>
</div>
<ion-label *ngIf="news.length === 0" class="centeredMessageContainer">
{{ 'search.nothing_found' | translate | titlecase }}
</ion-label>
<ion-infinite-scroll id="infinite-scroll" threshold="20%" (ionInfinite)="loadMore($event.target)">
<ion-infinite-scroll-content loading-spinner="crescent"> </ion-infinite-scroll-content>
</ion-infinite-scroll>
</div> </div>
<ion-label *ngIf="news.length === 0" class="centeredMessageContainer">
{{ 'search.nothing_found' | translate | titlecase }}
</ion-label>
<ion-infinite-scroll id="infinite-scroll" threshold="20%" (ionInfinite)="loadMore($event.target)">
<ion-infinite-scroll-content loading-spinner="crescent"> </ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content> </ion-content>

View File

@@ -22,101 +22,97 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content color="light" parallax [parallaxSize]="130">
<div> <section class="user-card-wrapper">
<section class="user-card-wrapper"> <ion-card class="user-card">
<ion-card class="user-card"> <ion-card-header>
<ion-card-header> <ion-img src="assets/imgs/header.svg"></ion-img>
<ion-img src="assets/imgs/header.svg"></ion-img> <span *ngIf="user$ | async as userInfo">
<span *ngIf="user$ | async as userInfo"> {{ userInfo.role ? (userInfo?.role | titlecase) : ('profile.role_guest' | translate | titlecase) }}
{{ </span>
userInfo.role ? (userInfo?.role | titlecase) : ('profile.role_guest' | translate | titlecase) </ion-card-header>
}} <ion-card-content>
</span> <ion-img class="profile-card-img" src="assets/imgs/profile-card-head.svg"></ion-img>
</ion-card-header> <ion-grid>
<ion-card-content> <ion-row>
<ion-img class="profile-card-img" src="assets/imgs/profile-card-head.svg"></ion-img> <ion-col size="3"></ion-col>
<ion-grid> <ion-col
<ion-row> *ngIf="data.default.loggedIn$ | async as loggedIn; else logInPrompt"
<ion-col size="3"></ion-col> size="9"
<ion-col class="main-info"
*ngIf="data.default.loggedIn$ | async as loggedIn; else logInPrompt" >
size="9" <ng-container *ngIf="user$ | async as userInfo">
class="main-info" <ion-text class="full-name">
> {{ userInfo?.name }}
<ng-container *ngIf="user$ | async as userInfo"> </ion-text>
<ion-text class="full-name"> <div class="matriculation-number">
{{ userInfo?.name }} <ion-label>
{{ 'profile.userInfo.studentId' | translate | uppercase }}
</ion-label>
<ion-text>
{{ userInfo?.studentId }}
</ion-text> </ion-text>
<div class="matriculation-number"> </div>
<ion-label> <div class="user-name">
{{ 'profile.userInfo.studentId' | translate | uppercase }} <ion-label>
</ion-label> {{ 'profile.userInfo.username' | translate | uppercase }}
<ion-text> </ion-label>
{{ userInfo?.studentId }} <ion-text>{{ userInfo?.id }}</ion-text>
</ion-text> </div>
</div> <div class="email">
<div class="user-name"> <ion-label>
<ion-label> {{ 'profile.userInfo.email' | translate | uppercase }}
{{ 'profile.userInfo.username' | translate | uppercase }} </ion-label>
</ion-label> <ion-text>
<ion-text>{{ userInfo?.id }}</ion-text> {{ userInfo?.email }}
</div> </ion-text>
<div class="email"> </div>
<ion-label> </ng-container>
{{ 'profile.userInfo.email' | translate | uppercase }} </ion-col>
</ion-label> <ng-template #logInPrompt>
<ion-text> <ion-col size="9">
{{ userInfo?.email }} <ion-text class="log-in-prompt">
</ion-text> {{ 'profile.userInfo.logInPrompt' | translate }}
</div> </ion-text>
</ng-container>
</ion-col> </ion-col>
<ng-template #logInPrompt> </ng-template>
<ion-col size="9"> </ion-row>
<ion-text class="log-in-prompt"> </ion-grid>
{{ 'profile.userInfo.logInPrompt' | translate }} </ion-card-content>
</ion-text> </ion-card>
</ion-col> </section>
</ng-template> <stapps-profile-page-section
</ion-row> *ngFor="let section of sections"
</ion-grid> [item]="section"
</ion-card-content> ></stapps-profile-page-section>
</ion-card> <section class="courses">
</section> <ion-label class="section-headline">
<stapps-profile-page-section {{ 'profile.titleCourses' | translate | uppercase }}
*ngFor="let section of sections" </ion-label>
[item]="section" <ion-card class="courses-card">
></stapps-profile-page-section> <ion-card-header (click)="toggleCourseCardState()">
<section class="courses"> <span>{{ 'profile.courses.today' | translate | uppercase }}</span>
<ion-label class="section-headline"> <ion-icon [name]="courseCardState" color="dark" size="20"></ion-icon>
{{ 'profile.titleCourses' | translate | uppercase }} </ion-card-header>
</ion-label> <ion-card-content class="course-card" [class.show-card]="courseCardState === courseCardEnum.expanded">
<ion-card class="courses-card"> <ng-container *ngIf="myCoursesToday.length === 0">
<ion-card-header (click)="toggleCourseCardState()"> <div class="no-course">
<span>{{ 'profile.courses.today' | translate | uppercase }}</span> {{ 'profile.courses.no_courses' | translate }}
<ion-icon [name]="courseCardState" color="dark" size="20"></ion-icon> </div>
</ion-card-header> </ng-container>
<ion-card-content class="course-card" [class.show-card]="courseCardState === courseCardEnum.expanded"> <ng-container *ngFor="let myCourse of myCoursesToday">
<ng-container *ngIf="myCoursesToday.length === 0"> <div class="clickable" [routerLink]="['/data-detail', myCourse.course.event.uid]">
<div class="no-course"> <div>{{ myCourse?.startTime }} - {{ myCourse?.endTime }}</div>
{{ 'profile.courses.no_courses' | translate }} <div>{{ myCourse?.course.event?.originalCategory }}</div>
<div [class.last]="!myCourse?.course.inPlace?.name">
{{ myCourse.course?.event?.name }}
</div> </div>
</ng-container> <div *ngIf="myCourse.course?.inPlace?.name" [class.last]="myCourse.course?.inPlace?.name">
<ng-container *ngFor="let myCourse of myCoursesToday"> {{ myCourse.course?.inPlace.name }}
<div class="clickable" [routerLink]="['/data-detail', myCourse.course.event.uid]">
<div>{{ myCourse?.startTime }} - {{ myCourse?.endTime }}</div>
<div>{{ myCourse?.course.event?.originalCategory }}</div>
<div [class.last]="!myCourse?.course.inPlace?.name">
{{ myCourse.course?.event?.name }}
</div>
<div *ngIf="myCourse.course?.inPlace?.name" [class.last]="myCourse.course?.inPlace?.name">
{{ myCourse.course?.inPlace.name }}
</div>
</div> </div>
</ng-container> </div>
</ion-card-content> </ng-container>
</ion-card> </ion-card-content>
</section> </ion-card>
</div> </section>
</ion-content> </ion-content>

View File

@@ -12,15 +12,7 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@import 'src/theme/common/ion-content-parallax';
:host { :host {
ion-content {
--background: var(--ion-color-light);
@include ion-content-parallax($content-size: 130px);
}
section { section {
margin-bottom: calc(2 * var(--spacing-lg) - var(--spacing-md)); margin-bottom: calc(2 * var(--spacing-lg) - var(--spacing-md));
padding: var(--spacing-md); padding: var(--spacing-md);

View File

@@ -22,12 +22,11 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-no-padding ion-content-parallax"> <ion-content parallax class="ion-no-padding">
<div> <div class="settings-content">
<div class="settings-content"> <ng-container *ngFor="let categoryKey of categoriesOrder">
<ng-container *ngFor="let categoryKey of categoriesOrder"> <ion-list *ngIf="objectKeys(settingsCache).includes(categoryKey)">
<ion-list *ngIf="objectKeys(settingsCache).includes(categoryKey)"> <!-- <ion-item-divider>
<!-- <ion-item-divider>
<h2> <h2>
{{ {{
'categories[0]' 'categories[0]'
@@ -41,19 +40,18 @@
}} }}
</h2> </h2>
</ion-item-divider> --> </ion-item-divider> -->
<stapps-settings-item <stapps-settings-item
*ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)" *ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)"
[setting]="settingsCache[categoryKey].settings[settingKeys]" [setting]="settingsCache[categoryKey].settings[settingKeys]"
></stapps-settings-item> ></stapps-settings-item>
</ion-list> </ion-list>
</ng-container> </ng-container>
<calendar-sync-settings></calendar-sync-settings> <calendar-sync-settings></calendar-sync-settings>
<ion-button expand="block" (click)="presentResetAlert()" fill="outline"> <ion-button expand="block" (click)="presentResetAlert()" fill="outline">
{{ 'settings.resetSettings' | translate }} {{ 'settings.resetSettings' | translate }}
<ion-icon slot="start" name="device_reset"></ion-icon> <ion-icon slot="start" name="device_reset"></ion-icon>
</ion-button> </ion-button>
</div>
</div> </div>
</ion-content> </ion-content>

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Directive, ElementRef, HostBinding, Input, OnDestroy, OnInit} from '@angular/core';
type IonicColor =
| 'danger'
| 'dark'
| 'light'
| 'medium'
| 'primary'
| 'secondary'
| 'success'
| 'tertiary'
| 'warning'
| string;
/**
* Adds a parallax effect to `<ion-content/>` elements
*
* Why is this necessary and why do we need to inject
* elements into a foreign element's shadow DOM just for this?
*
* We previously had used a global style in conjunction with
* adding a child `<div/>`, however, unfortunately Safari 16.4
* broke 3D transforms where the parent is inside the shadow
* DOM and child elements are outside.
*
* We also have to use `<ion-content/>` as ionic relies on the
* `<ion-content/>` selector to make iOS animations work.
*
* Which means this is pretty much the only way to solve this
* problem.
*
* This directive, when applied using `<ion-content parallax/>`,
* will transform its shadow DOM from
*
* ```html
* <div id="background-content" part="background"></div>
* <main class="inner-scroll scroll-y" part="scroll">
* <slot></slot>
* </main>
* <slot name="fixed"><slot />
* ```
*
* To
*
* ```html
* <div id="background-content" part="background"></div>
* <main class="inner-scroll scroll-y" part="scroll parallax-scroll">
* <div part="parallax-parent">
* <div part="parallax"></div>
* <slot></slot>
* </div>
* </main>
* <slot name="fixed"><slot />
* ```
*
* Which we can then be used through `::part()` to style it from
* a global style sheet.
*/
@Directive({
selector: 'ion-content[parallax]',
})
export class IonContentParallaxDirective implements OnInit, OnDestroy {
@HostBinding('style.--parallax-content-size.px') @Input() parallaxSize = 230;
@Input() set parallaxColor(value: IonicColor) {
this.parallaxBackground = `var(--ion-color-${value})`;
}
@HostBinding('style.--parallax-background') parallaxBackground?: string;
private mutationObserver: MutationObserver;
constructor(private element: ElementRef) {}
ngOnInit() {
this.mutationObserver = new MutationObserver(() => {
const inner = this.element.nativeElement.shadowRoot.querySelector('.inner-scroll') as
| HTMLDivElement
| undefined;
if (!inner) return;
// eslint-disable-next-line unicorn/no-array-for-each
inner.childNodes.forEach(node => node.remove());
inner.part.add('parallax-scroll');
const parallaxParentElement = document.createElement('div');
parallaxParentElement.part.add('parallax-parent');
const parallaxElement = document.createElement('div');
parallaxElement.part.add('parallax');
inner.append(parallaxParentElement);
parallaxParentElement.append(parallaxElement, document.createElement('slot'));
});
this.mutationObserver.observe(this.element.nativeElement.shadowRoot, {
childList: true,
});
}
ngOnDestroy() {
this.mutationObserver.disconnect();
}
}

View File

@@ -31,10 +31,12 @@ import {SimpleSwiperComponent} from './simple-swiper.component';
import {SearchbarAutofocusDirective} from './searchbar-autofocus.directive'; import {SearchbarAutofocusDirective} from './searchbar-autofocus.directive';
import {SectionComponent} from './section.component'; import {SectionComponent} from './section.component';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {IonContentParallaxDirective} from './ion-content-parallax.directive';
@NgModule({ @NgModule({
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild(), RouterModule], imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild(), RouterModule],
declarations: [ declarations: [
IonContentParallaxDirective,
ElementSizeChangeDirective, ElementSizeChangeDirective,
ArrayLastPipe, ArrayLastPipe,
DateIsThisPipe, DateIsThisPipe,
@@ -50,6 +52,7 @@ import {RouterModule} from '@angular/router';
SearchbarAutofocusDirective, SearchbarAutofocusDirective,
], ],
exports: [ exports: [
IonContentParallaxDirective,
ElementSizeChangeDirective, ElementSizeChangeDirective,
ArrayLastPipe, ArrayLastPipe,
DateIsThisPipe, DateIsThisPipe,

View File

@@ -124,10 +124,6 @@ ion-header {
} }
} }
.ion-content-parallax {
@include ion-content-parallax();
}
ion-alert { ion-alert {
button.alert-button.preferred { button.alert-button.preferred {
background-color: var(--ion-color-primary); background-color: var(--ion-color-primary);

View File

@@ -12,43 +12,34 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
$overscroll-padding: 720px;
$parallax-strength: 2;
$default-parallax-content-size: 230px;
@mixin ion-content-parallax( ion-content::part(parallax-scroll) {
$parallax-background: var(--ion-color-primary), perspective: 2px;
$background: var(--ion-background-color), perspective-origin: center top;
$parallax-strength: 2, }
$overscroll-padding: 720px,
$content-size: 230px ion-content::part(parallax-parent) {
) { transform-style: preserve-3d;
&::part(background) { position: relative;
background: $background; }
}
&::part(scroll) { ion-content::part(parallax) {
perspective: 2px; position: absolute;
perspective-origin: center top; top: 0;
} right: 0;
> div { left: 0;
transform-style: preserve-3d;
position: relative; $translateY: calc($overscroll-padding * $parallax-strength);
$translateZ: calc(-1px * $parallax-strength);
&::before { $transform-origin: calc($parallax-strength * $parallax-strength * $overscroll-padding);
content: ' ';
position: absolute; height: calc(var(--parallax-content-size, $default-parallax-content-size) + $overscroll-padding);
top: 0; width: 150%;
right: 0; transform-origin: 50% $transform-origin;
left: 0; transform: translate3d(0px, $translateY, $translateZ) scale($parallax-strength);
$height: calc($content-size + $overscroll-padding); background: var(--parallax-background, var(--ion-color-primary));
$translateY: calc($overscroll-padding * $parallax-strength);
$translateZ: calc(-1px * $parallax-strength);
$transform-origin: calc($parallax-strength * $parallax-strength * $overscroll-padding);
height: $height;
width: 150%;
transform-origin: 50% $transform-origin;
transform: translate3d(0px, $translateY, $translateZ) scale($parallax-strength);
background: $parallax-background;
}
}
} }