MOBILE-4290 login: Collapse site help questions
This commit is contained in:
		
							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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user