MOBILE-4290 login: Collapse site help questions
parent
bfe9100879
commit
b382486580
|
@ -4,7 +4,7 @@
|
|||
<h1>{{ 'core.login.help' | translate }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="closeHelp()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-button fill="clear" (click)="close()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
|
@ -12,58 +12,26 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.login.faqwhatisurlquestion' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p [innerHTML]="'core.login.faqwhatisurlanswer' | translate: {$image: urlImageHtml}"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.login.faqcannotfindmysitequestion' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p>{{ 'core.login.faqcannotfindmysiteanswer' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.login.faqsetupsitequestion' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p>
|
||||
<core-format-text [text]="'core.login.faqsetupsiteanswer' | translate:{$link: setupLinkHtml}" [filter]="false">
|
||||
<ng-container *ngFor="let question of questions">
|
||||
<ion-item button class="ion-text-wrap divider" (click)="toggle(question)" sticky="true" [attr.aria-expanded]="isOpen(question)"
|
||||
[attr.aria-controls]="'question-' + question.id + '-answer'" role="heading" detail="false">
|
||||
<ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon"
|
||||
[class.expandable-status-icon-expanded]="isOpen(question)">
|
||||
</ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ question.text }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item [id]="'question-' + question.id + '-answer'"
|
||||
[class]="question.answer.class + 'ion-text-wrap core-login-site-help--answer'" [class.open]="isOpen(question)"
|
||||
[tabindex]="isOpen(question) ? null : -1" [attr.inert]="isOpen(question) ? null : 'true'">
|
||||
<ion-label>
|
||||
<p *ngIf="question.answer.format === 'text'">{{ question.answer.text }}</p>
|
||||
<p *ngIf="question.answer.format === 'safe-html'" [innerHTML]="question.answer.text"></p>
|
||||
<core-format-text *ngIf="question.answer.format === 'unsafe-html'" [text]="question.answer.text" [filter]="false">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.login.faqtestappquestion' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p [innerHTML]="'core.login.faqtestappanswer' | translate"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="canScanQR">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.login.faqwhereisqrcode' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap core-login-faqwhereisqrcodeanswer" *ngIf="canScanQR">
|
||||
<ion-label>
|
||||
<p [innerHTML]="'core.login.faqwhereisqrcodeanswer' | translate: {$image: qrCodeImageHtml}"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
|
|
|
@ -1,13 +1,38 @@
|
|||
.core-login-faqwhatisurlanswer img {
|
||||
max-height: 50px;
|
||||
}
|
||||
:host {
|
||||
|
||||
.core-login-faqwhereisqrcodeanswer img {
|
||||
max-height: 220px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.core-login-faqwhatisurlanswer img {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.core-login-faqwhereisqrcodeanswer img {
|
||||
max-height: 220px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&:not(.hydrated) {
|
||||
|
||||
.core-login-site-help--answer {
|
||||
opacity: 0;
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.hydrated {
|
||||
|
||||
.core-login-site-help--answer {
|
||||
height: 0;
|
||||
transition: height 200ms ease-in-out;
|
||||
|
||||
&.open {
|
||||
height: var(--height);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { AfterViewInit, Component, ElementRef, HostBinding, OnDestroy } from '@angular/core';
|
||||
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ModalController, Translate } from '@singletons';
|
||||
import { CoreLoginHelperProvider, GET_STARTED_URL } from '@features/login/services/login-helper';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||
|
||||
/**
|
||||
* Component that displays help to connect to a site.
|
||||
|
@ -26,27 +28,187 @@ import { CoreLoginHelperProvider, GET_STARTED_URL } from '@features/login/servic
|
|||
templateUrl: 'site-help.html',
|
||||
styleUrls: ['site-help.scss'],
|
||||
})
|
||||
export class CoreLoginSiteHelpComponent {
|
||||
export class CoreLoginSiteHelpComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
urlImageHtml: string;
|
||||
setupLinkHtml: string;
|
||||
qrCodeImageHtml: string;
|
||||
canScanQR: boolean;
|
||||
openQuestion?: number;
|
||||
questions: Question[] = [];
|
||||
@HostBinding('class.hydrated') hydrated = false;
|
||||
|
||||
constructor() {
|
||||
private promises: CoreCancellablePromise[] = [];
|
||||
|
||||
constructor(protected el: ElementRef<HTMLElement>) {
|
||||
const getStartedTitle = Translate.instant('core.login.faqsetupsitelinktitle');
|
||||
const canScanQR = !CoreUtils.canScanQR();
|
||||
const urlImageHtml = CoreLoginHelperProvider.FAQ_URL_IMAGE_HTML;
|
||||
const qrCodeImageHtml = CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML;
|
||||
const setupLinkHtml = `<a href="${GET_STARTED_URL}" title="${getStartedTitle}">${GET_STARTED_URL}</a>`;
|
||||
const questions: Array<QuestionDefinition | false> = [
|
||||
{
|
||||
text: Translate.instant('core.login.faqwhatisurlquestion'),
|
||||
answer: {
|
||||
text: Translate.instant('core.login.faqwhatisurlanswer', { $image: urlImageHtml }),
|
||||
format: AnswerFormat.SafeHTML,
|
||||
class: 'core-login-faqwhatisurlanswer',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: Translate.instant('core.login.faqcannotfindmysitequestion'),
|
||||
answer: {
|
||||
text: Translate.instant('core.login.faqcannotfindmysiteanswer'),
|
||||
format: AnswerFormat.Text,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: Translate.instant('core.login.faqsetupsitequestion'),
|
||||
answer: {
|
||||
text: Translate.instant('core.login.faqsetupsiteanswer', { $link: setupLinkHtml }),
|
||||
format: AnswerFormat.UnsafeHTML,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: Translate.instant('core.login.faqtestappquestion'),
|
||||
answer: {
|
||||
text: Translate.instant('core.login.faqtestappanswer'),
|
||||
format: AnswerFormat.SafeHTML,
|
||||
},
|
||||
},
|
||||
canScanQR && {
|
||||
text: Translate.instant('core.login.faqwhereisqrcode'),
|
||||
answer: {
|
||||
text: Translate.instant('core.login.faqwhereisqrcodeanswer', { $image: qrCodeImageHtml }),
|
||||
format: AnswerFormat.SafeHTML,
|
||||
class: 'core-login-faqwhereisqrcodeanswer',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.canScanQR = CoreUtils.canScanQR();
|
||||
this.urlImageHtml = CoreLoginHelperProvider.FAQ_URL_IMAGE_HTML;
|
||||
this.qrCodeImageHtml = CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML;
|
||||
this.setupLinkHtml = `<a href="${GET_STARTED_URL}" title="${getStartedTitle}">${GET_STARTED_URL}</a>`;
|
||||
for (const question of questions) {
|
||||
if (!question) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.questions.push({
|
||||
...question,
|
||||
id: this.questions.length + 1,
|
||||
answer: {
|
||||
...question.answer,
|
||||
class: question.answer.class ?? '',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
const answers = Array.from(this.el.nativeElement.querySelectorAll<HTMLElement>('.core-login-site-help--answer'));
|
||||
|
||||
await Promise.all(answers.map(async answer => {
|
||||
await this.track(CoreUtils.waitFor(() => answer.clientHeight !== 0));
|
||||
await this.track(CoreDomUtils.waitForImages(answer));
|
||||
|
||||
answer.style.setProperty('--height', `${answer.clientHeight}px`);
|
||||
}));
|
||||
|
||||
this.hydrated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.promises.forEach(promise => promise.cancel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given question is open or not.
|
||||
*
|
||||
* @param question Question.
|
||||
* @returns Whether the given question is open.
|
||||
*/
|
||||
isOpen(question: Question): boolean {
|
||||
return this.openQuestion === question.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle question.
|
||||
*
|
||||
* @param question Question to toggle.
|
||||
*/
|
||||
toggle(question: Question): void {
|
||||
if (question.id === this.openQuestion) {
|
||||
delete this.openQuestion;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.openQuestion = question.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close help modal.
|
||||
*/
|
||||
closeHelp(): void {
|
||||
close(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a promise for cleanup.
|
||||
*
|
||||
* @param promise Cancellable promise.
|
||||
* @returns The promise.
|
||||
*/
|
||||
protected track<T>(promise: CoreCancellablePromise<T>): Promise<T> {
|
||||
const remove = () => {
|
||||
const index = this.promises.indexOf(promise);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.promises.splice(index, 1);
|
||||
};
|
||||
|
||||
this.promises.push(promise);
|
||||
|
||||
promise.then(remove).catch(remove);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Question data.
|
||||
*/
|
||||
interface Question {
|
||||
id: number;
|
||||
text: string;
|
||||
answer: Answer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Question answer.
|
||||
*/
|
||||
interface Answer {
|
||||
text: string;
|
||||
class: string;
|
||||
format: AnswerFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Question answer format.
|
||||
*/
|
||||
enum AnswerFormat {
|
||||
Text = 'text',
|
||||
SafeHTML = 'safe-html',
|
||||
UnsafeHTML = 'unsafe-html',
|
||||
}
|
||||
|
||||
/**
|
||||
* Question definition.
|
||||
*/
|
||||
type QuestionDefinition = Omit<Question, 'id' | 'answer'> & {
|
||||
answer: Omit<Answer, 'class'> & Partial<Pick<Answer, 'class'>>;
|
||||
};
|
||||
|
|
|
@ -38,6 +38,7 @@ import { CorePlatform } from '@services/platform';
|
|||
import { CoreErrorWithOptions } from '@classes/errors/errorwithtitle';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||
|
||||
export type TreeNode<T> = T & { children: TreeNode<T>[] };
|
||||
|
||||
|
@ -1795,6 +1796,34 @@ export class CoreUtilsProvider {
|
|||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until a given condition is met.
|
||||
*
|
||||
* @param condition Condition.
|
||||
* @returns Cancellable promise.
|
||||
*/
|
||||
waitFor(condition: () => boolean, interval: number = 50): CoreCancellablePromise<void> {
|
||||
if (condition()) {
|
||||
return CoreCancellablePromise.resolve();
|
||||
}
|
||||
|
||||
let intervalId: number | undefined;
|
||||
|
||||
return new CoreCancellablePromise<void>(
|
||||
async (resolve) => {
|
||||
intervalId = window.setInterval(() => {
|
||||
if (!condition()) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
window.clearInterval(intervalId);
|
||||
}, interval);
|
||||
},
|
||||
() => window.clearInterval(intervalId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the next tick.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue