diff --git a/src/core/features/login/components/site-help/site-help.html b/src/core/features/login/components/site-help/site-help.html index cb711026d..949754fd8 100644 --- a/src/core/features/login/components/site-help/site-help.html +++ b/src/core/features/login/components/site-help/site-help.html @@ -4,7 +4,7 @@

{{ 'core.login.help' | translate }}

- + @@ -12,58 +12,26 @@ - - -

{{ 'core.login.faqwhatisurlquestion' | translate }}

-
-
- - -

-
-
- - -

{{ 'core.login.faqcannotfindmysitequestion' | translate }}

-
-
- - -

{{ 'core.login.faqcannotfindmysiteanswer' | translate }}

-
-
- - -

{{ 'core.login.faqsetupsitequestion' | translate }}

-
-
- - -

- + + + + +

{{ question.text }}

+
+
+ + +

{{ question.answer.text }}

+

+ -

-
-
- - -

{{ 'core.login.faqtestappquestion' | translate }}

-
-
- - -

-
-
- - -

{{ 'core.login.faqwhereisqrcode' | translate }}

-
-
- + + +
diff --git a/src/core/features/login/components/site-help/site-help.scss b/src/core/features/login/components/site-help/site-help.scss index c3eb78ce7..b92178674 100644 --- a/src/core/features/login/components/site-help/site-help.scss +++ b/src/core/features/login/components/site-help/site-help.scss @@ -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; } diff --git a/src/core/features/login/components/site-help/site-help.ts b/src/core/features/login/components/site-help/site-help.ts index 35cf48cd6..4a3b22a91 100644 --- a/src/core/features/login/components/site-help/site-help.ts +++ b/src/core/features/login/components/site-help/site-help.ts @@ -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) { 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 = `${GET_STARTED_URL}`; + const questions: Array = [ + { + 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 = `${GET_STARTED_URL}`; + 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 { + const answers = Array.from(this.el.nativeElement.querySelectorAll('.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(promise: CoreCancellablePromise): Promise { + 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 & { + answer: Omit & Partial>; +}; diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 39ef612e6..6f71dd6ee 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -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 & { children: TreeNode[] }; @@ -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 { + if (condition()) { + return CoreCancellablePromise.resolve(); + } + + let intervalId: number | undefined; + + return new CoreCancellablePromise( + async (resolve) => { + intervalId = window.setInterval(() => { + if (!condition()) { + return; + } + + resolve(); + window.clearInterval(intervalId); + }, interval); + }, + () => window.clearInterval(intervalId), + ); + } + /** * Wait until the next tick. *