MOBILE-4290 login: Collapse site help questions
parent
bfe9100879
commit
b382486580
|
@ -4,7 +4,7 @@
|
||||||
<h1>{{ 'core.login.help' | translate }}</h1>
|
<h1>{{ 'core.login.help' | translate }}</h1>
|
||||||
</ion-title>
|
</ion-title>
|
||||||
<ion-buttons slot="end">
|
<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-icon slot="icon-only" name="fas-xmark" aria-hidden="true"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
|
@ -12,58 +12,26 @@
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-item class="ion-text-wrap">
|
<ng-container *ngFor="let question of questions">
|
||||||
<ion-label>
|
<ion-item button class="ion-text-wrap divider" (click)="toggle(question)" sticky="true" [attr.aria-expanded]="isOpen(question)"
|
||||||
<h2>{{ 'core.login.faqwhatisurlquestion' | translate }}</h2>
|
[attr.aria-controls]="'question-' + question.id + '-answer'" role="heading" detail="false">
|
||||||
</ion-label>
|
<ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon"
|
||||||
</ion-item>
|
[class.expandable-status-icon-expanded]="isOpen(question)">
|
||||||
<ion-item class="ion-text-wrap">
|
</ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p [innerHTML]="'core.login.faqwhatisurlanswer' | translate: {$image: urlImageHtml}"></p>
|
<h2>{{ question.text }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item [id]="'question-' + question.id + '-answer'"
|
||||||
<ion-label>
|
[class]="question.answer.class + 'ion-text-wrap core-login-site-help--answer'" [class.open]="isOpen(question)"
|
||||||
<h2>{{ 'core.login.faqcannotfindmysitequestion' | translate }}</h2>
|
[tabindex]="isOpen(question) ? null : -1" [attr.inert]="isOpen(question) ? null : 'true'">
|
||||||
</ion-label>
|
<ion-label>
|
||||||
</ion-item>
|
<p *ngIf="question.answer.format === 'text'">{{ question.answer.text }}</p>
|
||||||
<ion-item class="ion-text-wrap">
|
<p *ngIf="question.answer.format === 'safe-html'" [innerHTML]="question.answer.text"></p>
|
||||||
<ion-label>
|
<core-format-text *ngIf="question.answer.format === 'unsafe-html'" [text]="question.answer.text" [filter]="false">
|
||||||
<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">
|
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</p>
|
</ion-label>
|
||||||
</ion-label>
|
</ion-item>
|
||||||
</ion-item>
|
</ng-container>
|
||||||
<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-list>
|
</ion-list>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -1,13 +1,38 @@
|
||||||
.core-login-faqwhatisurlanswer img {
|
:host {
|
||||||
max-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-login-faqwhereisqrcodeanswer img {
|
.core-login-faqwhatisurlanswer img {
|
||||||
max-height: 220px;
|
max-height: 50px;
|
||||||
margin-top: 5px;
|
}
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
.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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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 { CoreUtils } from '@services/utils/utils';
|
||||||
import { ModalController, Translate } from '@singletons';
|
import { ModalController, Translate } from '@singletons';
|
||||||
import { CoreLoginHelperProvider, GET_STARTED_URL } from '@features/login/services/login-helper';
|
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.
|
* 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',
|
templateUrl: 'site-help.html',
|
||||||
styleUrls: ['site-help.scss'],
|
styleUrls: ['site-help.scss'],
|
||||||
})
|
})
|
||||||
export class CoreLoginSiteHelpComponent {
|
export class CoreLoginSiteHelpComponent implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
urlImageHtml: string;
|
openQuestion?: number;
|
||||||
setupLinkHtml: string;
|
questions: Question[] = [];
|
||||||
qrCodeImageHtml: string;
|
@HostBinding('class.hydrated') hydrated = false;
|
||||||
canScanQR: boolean;
|
|
||||||
|
|
||||||
constructor() {
|
private promises: CoreCancellablePromise[] = [];
|
||||||
|
|
||||||
|
constructor(protected el: ElementRef<HTMLElement>) {
|
||||||
const getStartedTitle = Translate.instant('core.login.faqsetupsitelinktitle');
|
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();
|
for (const question of questions) {
|
||||||
this.urlImageHtml = CoreLoginHelperProvider.FAQ_URL_IMAGE_HTML;
|
if (!question) {
|
||||||
this.qrCodeImageHtml = CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML;
|
continue;
|
||||||
this.setupLinkHtml = `<a href="${GET_STARTED_URL}" title="${getStartedTitle}">${GET_STARTED_URL}</a>`;
|
}
|
||||||
|
|
||||||
|
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.
|
* Close help modal.
|
||||||
*/
|
*/
|
||||||
closeHelp(): void {
|
close(): void {
|
||||||
ModalController.dismiss();
|
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 { CoreErrorWithOptions } from '@classes/errors/errorwithtitle';
|
||||||
import { CoreFilepool } from '@services/filepool';
|
import { CoreFilepool } from '@services/filepool';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||||
|
|
||||||
export type TreeNode<T> = T & { children: TreeNode<T>[] };
|
export type TreeNode<T> = T & { children: TreeNode<T>[] };
|
||||||
|
|
||||||
|
@ -1795,6 +1796,34 @@ export class CoreUtilsProvider {
|
||||||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
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.
|
* Wait until the next tick.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue