commit
c3bd315355
|
@ -2046,6 +2046,9 @@
|
|||
"core.login.exceededpasswordresetattemptssupportsubject": "local_moodlemobileapp",
|
||||
"core.login.faqcannotfindmysiteanswer": "local_moodlemobileapp",
|
||||
"core.login.faqcannotfindmysitequestion": "local_moodlemobileapp",
|
||||
"core.login.faqcantloginanswer": "local_moodlemobileapp",
|
||||
"core.login.faqcantloginquestion": "local_moodlemobileapp",
|
||||
"core.login.faqmore": "local_moodlemobileapp",
|
||||
"core.login.faqsetupsiteanswer": "local_moodlemobileapp",
|
||||
"core.login.faqsetupsitelinktitle": "local_moodlemobileapp",
|
||||
"core.login.faqsetupsitequestion": "local_moodlemobileapp",
|
||||
|
|
|
@ -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,57 +12,30 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<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>{{ 'core.login.faqwhatisurlquestion' | translate }}</h2>
|
||||
<h2>{{ question.text }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<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 [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">
|
||||
<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">
|
||||
</ng-container>
|
||||
<ion-item>
|
||||
<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>
|
||||
<p [innerHTML]="'core.login.faqmore' | translate"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
:host {
|
||||
|
||||
.core-login-faqwhatisurlanswer img {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
@ -8,6 +10,29 @@
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
&: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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,194 @@ 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.faqcantloginquestion'),
|
||||
answer: {
|
||||
text: Translate.instant('core.login.faqcantloginanswer'),
|
||||
format: AnswerFormat.SafeHTML,
|
||||
},
|
||||
},
|
||||
{
|
||||
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'>>;
|
||||
};
|
||||
|
|
|
@ -35,14 +35,17 @@
|
|||
"exceededloginattemptswithoutsupport": "Need help logging in? Try {{recoverPassword}}.",
|
||||
"exceededpasswordresetattempts": "It seems you are having trouble accessing your account. You can contact your school or learning provider or try again later.",
|
||||
"exceededpasswordresetattemptssupportsubject": "I can't reset my password",
|
||||
"faqcannotfindmysiteanswer": "If you still can't find it, please contact your school or learning provider for help.",
|
||||
"faqcannotfindmysitequestion": "I can't find my site by address.",
|
||||
"faqcannotfindmysiteanswer": "If you tried searching by URL address and still can't find your Moodle site, please get in touch with the person who takes care of Moodle in your school or learning organisation.",
|
||||
"faqcannotfindmysitequestion": "I can't find my site by URL address.",
|
||||
"faqcantloginanswer": "<p>Once you've connected to your Moodle site, you should be able to log in with your usual username and password.</p><br><p>If you forgot your username or password, select the option <strong>Forgotten your username or password?</strong>. If you still have trouble logging in or can't see any options for retrieving your username or password, please get in touch with the person who takes care of Moodle in your school or learning organisation.</p>",
|
||||
"faqcantloginquestion": "I can't log in.",
|
||||
"faqmore": "Check out <a href=\"https://docs.moodle.org/en/Moodle_app_FAQ\" target=\"_blank\">our FAQ</a> for more answers.",
|
||||
"faqsetupsiteanswer": "Visit {{$link}} to check out the different options you have to create your own Moodle site.",
|
||||
"faqsetupsitelinktitle": "Get started.",
|
||||
"faqsetupsitequestion": "I want to set up my own Moodle site.",
|
||||
"faqtestappanswer": "To test the app in a Moodle demo site, type \"teacher\" or \"student\" in the <strong>Your site</strong> field and tap <strong>Connect to your site</strong>.",
|
||||
"faqtestappquestion": "Can I test the app on a demo site?",
|
||||
"faqwhatisurlanswer": "<p>If you can't find your site by name, try searching by address instead.</p><p>To find your site address:</p><ol><li>Open a web browser and go to your Moodle site login page.</li><li>At the top of the page, in the address bar, you will see the URL of your Moodle site e.g. \"campus.example.edu\".<br>{{$image}}</li><li>Copy the address (don't copy the /login nor what comes after), paste it into <strong>Your site</strong> in the app, then tap <strong>Connect to your site</strong></li><li>Now you can log in to your site with your username and password.</li></ol>",
|
||||
"faqwhatisurlanswer": "<p>If you can't find your site by name, try searching by your Moodle site URL address (or web address) address instead.</p><p>To find your site address:</p><ol><li>Open a web browser and go to your Moodle site login page.</li><li>At the top of the page, in the address bar, you'll see the URL address of your Moodle site, e.g. \"campus.example.edu\".<br>{{$image}}</li><li>Copy the URL (don't copy the /login nor what comes after), paste it into <strong>Your site</strong> in the app, then tap <strong>Connect to your site</strong></li><li>Now you can log in to your site with your username and password.</li></ol>",
|
||||
"faqwhatisurlquestion": "How can I find my site?",
|
||||
"faqwhereisqrcode": "Where can I find the QR code?",
|
||||
"faqwhereisqrcodeanswer": "<p>If your school or learning provider has enabled it, you will find a QR code on the web site at the bottom of your user profile page.</p>{{$image}}",
|
||||
|
|
|
@ -59,6 +59,7 @@ import { CoreUserSupport } from '@features/user/services/support';
|
|||
import { CoreErrorInfoComponent } from '@components/error-info/error-info';
|
||||
import { CorePlatform } from '@services/platform';
|
||||
import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2';
|
||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||
|
||||
/*
|
||||
* "Utils" service with helper functions for UI, DOM elements and HTML code.
|
||||
|
@ -1918,32 +1919,62 @@ export class CoreDomUtilsProvider {
|
|||
* @param element The element to search in.
|
||||
* @returns Promise resolved with a boolean: whether there was any image to load.
|
||||
*/
|
||||
async waitForImages(element: HTMLElement): Promise<boolean> {
|
||||
waitForImages(element: HTMLElement): CoreCancellablePromise<boolean> {
|
||||
const imgs = Array.from(element.querySelectorAll('img'));
|
||||
const promises: Promise<void>[] = [];
|
||||
let hasImgToLoad = false;
|
||||
|
||||
imgs.forEach((img) => {
|
||||
if (img && !img.complete) {
|
||||
hasImgToLoad = true;
|
||||
if (imgs.length === 0) {
|
||||
return CoreCancellablePromise.resolve(false);
|
||||
}
|
||||
|
||||
// Wait for image to load or fail.
|
||||
promises.push(new Promise((resolve) => {
|
||||
const imgLoaded = (): void => {
|
||||
resolve();
|
||||
img.removeEventListener('load', imgLoaded);
|
||||
img.removeEventListener('error', imgLoaded);
|
||||
let completedImages = 0;
|
||||
let waitedForImages = false;
|
||||
const listeners: WeakMap<Element, () => unknown> = new WeakMap();
|
||||
const imageCompleted = (resolve: (result: boolean) => void) => {
|
||||
completedImages++;
|
||||
|
||||
if (completedImages === imgs.length) {
|
||||
resolve(waitedForImages);
|
||||
}
|
||||
};
|
||||
|
||||
img.addEventListener('load', imgLoaded);
|
||||
img.addEventListener('error', imgLoaded);
|
||||
}));
|
||||
return new CoreCancellablePromise<boolean>(
|
||||
resolve => {
|
||||
for (const img of imgs) {
|
||||
if (!img || img.complete) {
|
||||
imageCompleted(resolve);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
waitedForImages = true;
|
||||
|
||||
// Wait for image to load or fail.
|
||||
const imgCompleted = (): void => {
|
||||
img.removeEventListener('load', imgCompleted);
|
||||
img.removeEventListener('error', imgCompleted);
|
||||
|
||||
imageCompleted(resolve);
|
||||
};
|
||||
|
||||
img.addEventListener('load', imgCompleted);
|
||||
img.addEventListener('error', imgCompleted);
|
||||
|
||||
listeners.set(img, imgCompleted);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
imgs.forEach(img => {
|
||||
const listener = listeners.get(img);
|
||||
|
||||
if (!listener) {
|
||||
return;
|
||||
}
|
||||
|
||||
img.removeEventListener('load', listener);
|
||||
img.removeEventListener('error', listener);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return hasImgToLoad;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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