Merge pull request #2698 from dpalou/MOBILE-3708

Mobile 3708
main
Dani Palou 2021-03-09 09:30:16 +01:00 committed by GitHub
commit 8471b549d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 118 additions and 106 deletions

View File

@ -6,9 +6,11 @@
<div *ngFor="let item of items"> <div *ngFor="let item of items">
<ion-card> <ion-card>
<ion-item class="core-course-module-handler item-media ion-text-wrap" detail="false" (click)="action($event, item)" <ion-item class="core-course-module-handler item-media ion-text-wrap" detail="false" (click)="action($event, item)"
[title]="item.name" button> button>
<img slot="start" [src]="item.iconUrl" alt="" role="presentation" *ngIf="item.iconUrl" class="core-module-icon"> <img slot="start" [src]="item.iconUrl" alt="" role="presentation" *ngIf="item.iconUrl" class="core-module-icon">
<ion-label> <ion-label>
<!-- Add the icon title so accessibility tools read it. -->
<span class="sr-only" *ngIf="item.iconTitle">{{ item.iconTitle }}</span>
<h2> <h2>
<core-format-text [text]="item.name" contextLevel="module" [contextInstanceId]="item.cmid" <core-format-text [text]="item.name" contextLevel="module" [contextInstanceId]="item.cmid"
[courseId]="item.courseid"></core-format-text> [courseId]="item.courseid"></core-format-text>

View File

@ -56,6 +56,7 @@ export class AddonBlockRecentlyAccessedItemsProvider {
const modicon = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'src'); const modicon = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'src');
item.iconUrl = CoreCourse.getModuleIconSrc(item.modname, modicon || undefined); item.iconUrl = CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
item.iconTitle = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'title');
return item; return item;
}); });
@ -99,4 +100,5 @@ export type AddonBlockRecentlyAccessedItemsItem = {
*/ */
export type AddonBlockRecentlyAccessedItemsItemCalculatedData = { export type AddonBlockRecentlyAccessedItemsItemCalculatedData = {
iconUrl: string; // Icon URL. Calculated by the app. iconUrl: string; // Icon URL. Calculated by the app.
iconTitle?: string | null; // Icon title.
}; };

View File

@ -58,7 +58,7 @@
<ion-label position="stacked">{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> <ion-label position="stacked">{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>
<core-show-password name="password"> <core-show-password name="password">
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
[core-auto-focus] #passwordinput [clearOnEdit]="false"> [autofocus]="true" #passwordinput [clearOnEdit]="false">
</ion-input> </ion-input>
</core-show-password> </core-show-password>
</ion-item> </ion-item>

View File

@ -424,7 +424,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
pageId = continueLast ? this.accessInfo.lastpageseen : this.accessInfo.firstpageid; pageId = continueLast ? this.accessInfo.lastpageseen : this.accessInfo.firstpageid;
} }
await CoreNavigator.navigate(`../player/${this.courseId}/${this.lesson.id}`, { await CoreNavigator.navigate('player', {
params: { params: {
pageId: pageId, pageId: pageId,
password: this.password, password: this.password,
@ -472,7 +472,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
return; return;
} }
CoreNavigator.navigate(`../player/${this.courseId}/${this.lesson.id}`, { CoreNavigator.navigate('player', {
params: { params: {
pageId: this.retakeToReview.pageid, pageId: this.retakeToReview.pageid,
password: this.password, password: this.password,
@ -695,7 +695,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async openRetake(userId: number): Promise<void> { async openRetake(userId: number): Promise<void> {
await CoreNavigator.navigate(`../user-retake/${this.courseId}/${this.lesson!.id}`, { await CoreNavigator.navigate('user-retake', {
params: { params: {
userId, userId,
}, },

View File

@ -15,7 +15,8 @@
<ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> <ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>
<core-show-password name="password"> <core-show-password name="password">
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
[core-auto-focus] #passwordinput [clearOnEdit]="false"></ion-input> [autofocus]="true" #passwordinput [clearOnEdit]="false">
</ion-input>
</core-show-password> </core-show-password>
</ion-item> </ion-item>
<ion-button expand="block" type="submit"> <ion-button expand="block" type="submit">

View File

@ -26,11 +26,11 @@ const routes: Routes = [
component: AddonModLessonIndexPage, component: AddonModLessonIndexPage,
}, },
{ {
path: 'player/:courseId/:lessonId', path: ':courseId/:cmId/player',
loadChildren: () => import('./pages/player/player.module').then( m => m.AddonModLessonPlayerPageModule), loadChildren: () => import('./pages/player/player.module').then( m => m.AddonModLessonPlayerPageModule),
}, },
{ {
path: 'user-retake/:courseId/:lessonId', path: ':courseId/:cmId/user-retake',
loadChildren: () => import('./pages/user-retake/user-retake.module').then( m => m.AddonModLessonUserRetakePageModule), loadChildren: () => import('./pages/user-retake/user-retake.module').then( m => m.AddonModLessonUserRetakePageModule),
}, },
]; ];

View File

@ -98,7 +98,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
mediaFile?: CoreWSExternalFile; // Media file of the lesson. mediaFile?: CoreWSExternalFile; // Media file of the lesson.
activityLink?: AddonModLessonActivityLink; // Next activity link data. activityLink?: AddonModLessonActivityLink; // Next activity link data.
protected lessonId!: number; // Lesson ID. protected cmId!: number; // Course module ID.
protected password?: string; // Lesson password (if any). protected password?: string; // Lesson password (if any).
protected forceLeave = false; // If true, don't perform any check when leaving the view. protected forceLeave = false; // If true, don't perform any check when leaving the view.
protected offline?: boolean; // Whether we are in offline mode. protected offline?: boolean; // Whether we are in offline mode.
@ -118,22 +118,19 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
* Component being initialized. * Component being initialized.
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.lessonId = CoreNavigator.getRouteNumberParam('lessonId')!; this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.password = CoreNavigator.getRouteParam('password'); this.password = CoreNavigator.getRouteParam('password');
this.review = !!CoreNavigator.getRouteBooleanParam('review'); this.review = !!CoreNavigator.getRouteBooleanParam('review');
this.currentPage = CoreNavigator.getRouteNumberParam('pageId'); this.currentPage = CoreNavigator.getRouteNumberParam('pageId');
this.retakeToReview = CoreNavigator.getRouteNumberParam('retake'); this.retakeToReview = CoreNavigator.getRouteNumberParam('retake');
// Block the lesson so it cannot be synced.
CoreSync.blockOperation(this.component, this.lessonId);
try { try {
// Fetch the Lesson data. // Fetch the Lesson data.
const success = await this.fetchLessonData(); const success = await this.fetchLessonData();
if (success) { if (success) {
// Review data loaded or new retake started, remove any retake being finished in sync. // Review data loaded or new retake started, remove any retake being finished in sync.
AddonModLessonSync.deleteRetakeFinishedInSync(this.lessonId); AddonModLessonSync.deleteRetakeFinishedInSync(this.lesson!.id);
} }
} finally { } finally {
this.loaded = true; this.loaded = true;
@ -144,8 +141,10 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
* Component being destroyed. * Component being destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.lesson) {
// Unblock the lesson so it can be synced. // Unblock the lesson so it can be synced.
CoreSync.unblockOperation(this.component, this.lessonId); CoreSync.unblockOperation(this.component, this.lesson.id);
}
} }
/** /**
@ -214,7 +213,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
// Get the possible jumps now. // Get the possible jumps now.
this.jumps = await AddonModLesson.getPagesPossibleJumps(this.lesson!.id, { this.jumps = await AddonModLesson.getPagesPossibleJumps(this.lesson!.id, {
cmId: this.lesson!.coursemodule, cmId: this.cmId,
readingStrategy: CoreSitesReadingStrategy.PreferCache, readingStrategy: CoreSitesReadingStrategy.PreferCache,
}); });
@ -259,14 +258,18 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
*/ */
protected async fetchLessonData(): Promise<boolean> { protected async fetchLessonData(): Promise<boolean> {
try { try {
// Wait for any ongoing sync to finish. We won't sync a lesson while it's being played. this.lesson = await AddonModLesson.getLesson(this.courseId, this.cmId);
await AddonModLessonSync.waitForSync(this.lessonId);
this.lesson = await AddonModLesson.getLessonById(this.courseId, this.lessonId);
this.title = this.lesson.name; // Temporary title. this.title = this.lesson.name; // Temporary title.
// Block the lesson so it cannot be synced.
CoreSync.blockOperation(this.component, this.lesson.id);
// Wait for any ongoing sync to finish. We won't sync a lesson while it's being played.
await AddonModLessonSync.waitForSync(this.lesson.id);
// If lesson has offline data already, use offline mode. // If lesson has offline data already, use offline mode.
this.offline = await AddonModLessonOffline.hasOfflineData(this.lessonId); this.offline = await AddonModLessonOffline.hasOfflineData(this.lesson.id);
if (!this.offline && !CoreApp.isOnline() && AddonModLesson.isLessonOffline(this.lesson) && if (!this.offline && !CoreApp.isOnline() && AddonModLesson.isLessonOffline(this.lesson) &&
!this.review) { !this.review) {
@ -275,7 +278,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
} }
const options = { const options = {
cmId: this.lesson.coursemodule, cmId: this.cmId,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
}; };
this.accessInfo = await this.callFunction<AddonModLessonGetAccessInformationWSResponse>( this.accessInfo = await this.callFunction<AddonModLessonGetAccessInformationWSResponse>(
@ -306,7 +309,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
// Lesson uses password, get the whole lesson object. // Lesson uses password, get the whole lesson object.
const options = { const options = {
password: this.password, password: this.password,
cmId: this.lesson.coursemodule, cmId: this.cmId,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
}; };
promises.push(this.callFunction<AddonModLessonLessonWSData>( promises.push(this.callFunction<AddonModLessonLessonWSData>(
@ -322,7 +325,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
if (this.offline) { if (this.offline) {
// Offline mode, get the list of possible jumps to allow navigation. // Offline mode, get the list of possible jumps to allow navigation.
promises.push(AddonModLesson.getPagesPossibleJumps(this.lesson.id, { promises.push(AddonModLesson.getPagesPossibleJumps(this.lesson.id, {
cmId: this.lesson.coursemodule, cmId: this.cmId,
readingStrategy: CoreSitesReadingStrategy.PreferCache, readingStrategy: CoreSitesReadingStrategy.PreferCache,
}).then((jumpList) => { }).then((jumpList) => {
this.jumps = jumpList; this.jumps = jumpList;
@ -344,7 +347,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
if (this.review && this.retakeToReview && CoreUtils.isWebServiceError(error)) { if (this.review && this.retakeToReview && CoreUtils.isWebServiceError(error)) {
// The user cannot review the retake. Unmark the retake as being finished in sync. // The user cannot review the retake. Unmark the retake as being finished in sync.
await AddonModLessonSync.deleteRetakeFinishedInSync(this.lessonId); await AddonModLessonSync.deleteRetakeFinishedInSync(this.lesson!.id);
} }
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
@ -373,7 +376,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
if (result?.warnings?.length) { if (result?.warnings?.length) {
// Some data was deleted. Check if the retake has changed. // Some data was deleted. Check if the retake has changed.
const info = await AddonModLesson.getAccessInformation(this.lesson!.id, { const info = await AddonModLesson.getAccessInformation(this.lesson!.id, {
cmId: this.lesson!.coursemodule, cmId: this.cmId,
}); });
if (info.attemptscount != this.accessInfo!.attemptscount) { if (info.attemptscount != this.accessInfo!.attemptscount) {
@ -485,7 +488,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
if (this.lesson!.timelimit && !this.accessInfo!.canmanage) { if (this.lesson!.timelimit && !this.accessInfo!.canmanage) {
// Get the last lesson timer. // Get the last lesson timer.
const timers = await AddonModLesson.getTimers(this.lesson!.id, { const timers = await AddonModLesson.getTimers(this.lesson!.id, {
cmId: this.lesson!.coursemodule, cmId: this.cmId,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
}); });
@ -510,12 +513,12 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
this.loadingMenu = true; this.loadingMenu = true;
const options = { const options = {
password: this.password, password: this.password,
cmId: this.lesson!.coursemodule, cmId: this.cmId,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
}; };
const pages = await this.callFunction<AddonModLessonGetPagesPageWSData[]>( const pages = await this.callFunction<AddonModLessonGetPagesPageWSData[]>(
AddonModLesson.getPages.bind(AddonModLesson.instance, this.lessonId, options), AddonModLesson.getPages.bind(AddonModLesson.instance, this.lesson!.id, options),
options, options,
); );
@ -543,7 +546,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
password: this.password, password: this.password,
review: this.review, review: this.review,
includeContents: true, includeContents: true,
cmId: this.lesson!.coursemodule, cmId: this.cmId,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
accessInfo: this.accessInfo, accessInfo: this.accessInfo,
jumps: this.jumps, jumps: this.jumps,
@ -638,15 +641,15 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
// Lesson allows offline and the user changed some data in server. Update cached data. // Lesson allows offline and the user changed some data in server. Update cached data.
const retake = this.accessInfo!.attemptscount; const retake = this.accessInfo!.attemptscount;
const options = { const options = {
cmId: this.lesson!.coursemodule, cmId: this.cmId,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
}; };
// Update in background the list of content pages viewed or question attempts. // Update in background the list of content pages viewed or question attempts.
if (AddonModLesson.isQuestionPage(this.pageData?.page?.type || -1)) { if (AddonModLesson.isQuestionPage(this.pageData?.page?.type || -1)) {
AddonModLesson.getQuestionsAttemptsOnline(this.lessonId, retake, options); AddonModLesson.getQuestionsAttemptsOnline(this.lesson!.id, retake, options);
} else { } else {
AddonModLesson.getContentPagesViewedOnline(this.lessonId, retake, options); AddonModLesson.getContentPagesViewedOnline(this.lesson!.id, retake, options);
} }
} }

View File

@ -55,7 +55,7 @@ export class AddonModLessonUserRetakePage implements OnInit {
loaded?: boolean; // Whether the data has been loaded. loaded?: boolean; // Whether the data has been loaded.
timeTakenReadable?: string; // Time taken in a readable format. timeTakenReadable?: string; // Time taken in a readable format.
protected lessonId!: number; // The lesson ID the retake belongs to. protected cmId!: number; // The lesson ID the retake belongs to.
protected userId?: number; // User ID to see the retakes. protected userId?: number; // User ID to see the retakes.
protected retakeNumber?: number; // Number of the initial retake to see. protected retakeNumber?: number; // Number of the initial retake to see.
protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed. protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed.
@ -64,7 +64,7 @@ export class AddonModLessonUserRetakePage implements OnInit {
* Component being initialized. * Component being initialized.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.lessonId = CoreNavigator.getRouteNumberParam('lessonId')!; this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId(); this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId();
this.retakeNumber = CoreNavigator.getRouteNumberParam('retake'); this.retakeNumber = CoreNavigator.getRouteNumberParam('retake');
@ -111,11 +111,11 @@ export class AddonModLessonUserRetakePage implements OnInit {
*/ */
protected async fetchData(): Promise<void> { protected async fetchData(): Promise<void> {
try { try {
this.lesson = await AddonModLesson.getLessonById(this.courseId, this.lessonId); this.lesson = await AddonModLesson.getLesson(this.courseId, this.cmId);
// Get the retakes overview for all participants. // Get the retakes overview for all participants.
const data = await AddonModLesson.getRetakesOverview(this.lesson.id, { const data = await AddonModLesson.getRetakesOverview(this.lesson.id, {
cmId: this.lesson.coursemodule, cmId: this.cmId,
}); });
// Search the student. // Search the student.
@ -185,8 +185,8 @@ export class AddonModLessonUserRetakePage implements OnInit {
protected async setRetake(retakeNumber: number): Promise<void> { protected async setRetake(retakeNumber: number): Promise<void> {
this.selectedRetake = retakeNumber; this.selectedRetake = retakeNumber;
const retakeData = await AddonModLesson.getUserRetake(this.lessonId, retakeNumber, { const retakeData = await AddonModLesson.getUserRetake(this.lesson!.id, retakeNumber, {
cmId: this.lesson!.coursemodule, cmId: this.cmId,
userId: this.userId, userId: this.userId,
}); });

View File

@ -65,7 +65,7 @@ export class AddonModLessonGradeLinkHandlerService extends CoreContentLinksModul
if (accessInfo.canviewreports) { if (accessInfo.canviewreports) {
// User can view reports, go to view the report. // User can view reports, go to view the report.
CoreNavigator.navigateToSitePath( CoreNavigator.navigateToSitePath(
AddonModLessonModuleHandlerService.PAGE_NAME + `/user-retake/${courseId}/${module.instance}`, AddonModLessonModuleHandlerService.PAGE_NAME + `/${courseId}/${module.id}/user-retake`,
{ {
params: { userId: Number(params.userid) }, params: { userId: Number(params.userid) },
siteId, siteId,

View File

@ -122,11 +122,9 @@ export class AddonModLessonReportLinkHandlerService extends CoreContentLinksHand
* *
* @param moduleId Module ID. * @param moduleId Module ID.
* @param userId User ID. * @param userId User ID.
* @param courseId Course ID.
* @param retake Retake to open. * @param retake Retake to open.
* @param groupId Group ID.
* @param siteId Site ID. * @param siteId Site ID.
* @param navCtrl The NavController to use to navigate. * @param courseId Course ID.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async openUserRetake( protected async openUserRetake(
@ -150,7 +148,7 @@ export class AddonModLessonReportLinkHandlerService extends CoreContentLinksHand
}; };
CoreNavigator.navigateToSitePath( CoreNavigator.navigateToSitePath(
AddonModLessonModuleHandlerService.PAGE_NAME + `/user-retake/${courseId}/${module.instance}`, AddonModLessonModuleHandlerService.PAGE_NAME + `/${courseId}/${module.id}/user-retake`,
{ params, siteId }, { params, siteId },
); );
} catch (error) { } catch (error) {

View File

@ -1486,7 +1486,7 @@ export class AddonModLessonProvider {
const response = await site.read<AddonModLessonGetLessonWSResponse>('mod_lesson_get_lesson', params, preSets); const response = await site.read<AddonModLessonGetLessonWSResponse>('mod_lesson_get_lesson', params, preSets);
if (typeof response.lesson.ongoing != 'undefined') { if (typeof response.lesson.ongoing == 'undefined') {
// Basic data not received, password is wrong. Remove stored password. // Basic data not received, password is wrong. Remove stored password.
this.removeStoredPassword(lessonId, site.id); this.removeStoredPassword(lessonId, site.id);

View File

@ -14,7 +14,7 @@
<!-- Form to edit the file's name. --> <!-- Form to edit the file's name. -->
<ion-input type="text" name="filename" [placeholder]="'core.filename' | translate" autocapitalize="none" autocorrect="off" <ion-input type="text" name="filename" [placeholder]="'core.filename' | translate" autocapitalize="none" autocorrect="off"
(click)="$event.stopPropagation()" [core-auto-focus] [(ngModel)]="newFileName" *ngIf="editMode"> (click)="$event.stopPropagation()" [autofocus]="true" [(ngModel)]="newFileName" *ngIf="editMode">
</ion-input> </ion-input>
<div class="buttons" slot="end" *ngIf="manage"> <div class="buttons" slot="end" *ngIf="manage">

View File

@ -1,5 +1,5 @@
<form #messageForm> <form #messageForm>
<textarea class="core-send-message-input" [core-auto-focus]="showKeyboard" [placeholder]="placeholder" rows="1" core-auto-rows <textarea class="core-send-message-input" [autofocus]="showKeyboard" [placeholder]="placeholder" rows="1" core-auto-rows
[(ngModel)]="message" name="message" (onResize)="textareaResized()" (keydown.enter)="enterClicked($event)" [(ngModel)]="message" name="message" (onResize)="textareaResized()" (keydown.enter)="enterClicked($event)"
(keydown.control.enter)="enterClicked($event, 'control')" (keydown.meta.enter)="enterClicked($event, 'meta')" (keydown.control.enter)="enterClicked($event, 'control')" (keydown.meta.enter)="enterClicked($event, 'meta')"
aria-multiline="true"></textarea> aria-multiline="true"></textarea>
@ -10,4 +10,3 @@
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
</form> </form>

View File

@ -21,6 +21,8 @@ import { CoreUtils } from '@services/utils/utils';
* Directive to auto focus an element when a view is loaded. * Directive to auto focus an element when a view is loaded.
* *
* You can apply it conditionallity assigning it a boolean value: <ion-input [core-auto-focus]="{{showKeyboard}}"> * You can apply it conditionallity assigning it a boolean value: <ion-input [core-auto-focus]="{{showKeyboard}}">
*
* @deprecated since 3.9.5. ion-input now supports an [autofocus] attribute, please use that one instead.
*/ */
@Directive({ @Directive({
selector: '[core-auto-focus]', selector: '[core-auto-focus]',
@ -39,16 +41,7 @@ export class CoreAutoFocusDirective implements OnInit {
* Component being initialized. * Component being initialized.
*/ */
ngOnInit(): void { ngOnInit(): void {
// @todo
// if (this.navCtrl.isTransitioning()) {
// // Navigating to a new page. Wait for the transition to be over.
// const subscription = this.navCtrl.viewDidEnter.subscribe(() => {
// this.autoFocus();
// subscription.unsubscribe();
// });
// } else {
this.autoFocus(); this.autoFocus();
// }
} }
/** /**

View File

@ -21,8 +21,9 @@
[class.core-section-download]="downloadEnabled"> [class.core-section-download]="downloadEnabled">
<ion-button class="core-button-select button-no-uppercase" (click)="showSectionSelector()" <ion-button class="core-button-select button-no-uppercase" (click)="showSectionSelector()"
aria-haspopup="true" [attr.aria-expanded]="sectionSelectorExpanded" aria-haspopup="true" [attr.aria-expanded]="sectionSelectorExpanded"
id="core-course-section-button" expand="block"> <!-- @todo: attr.aria-label? --> id="core-course-section-button" expand="block">
<ion-icon name="fas-folder" slot="start"></ion-icon> <ion-icon name="fas-folder" slot="start" aria-hidden="true"></ion-icon>
<span class="sr-only" *ngIf="selectedSection">{{ 'core.course.sections' | translate }}:</span>
<span class="core-button-select-text"> <span class="core-button-select-text">
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course" <core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true"> [contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">

View File

@ -9,23 +9,25 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-list id="core-course-section-selector" role="menu"> <ion-list id="core-course-section-selector">
<ng-container *ngFor="let section of sections"> <ng-container *ngFor="let section of sections">
<ion-item *ngIf="!section.hiddenbynumsections && section.id != stealthModulesSectionId" class="ion-text-wrap" <ion-item *ngIf="!section.hiddenbynumsections && section.id != stealthModulesSectionId" class="ion-text-wrap"
(click)="selectSection(section)" [class.core-selected-item]="selected?.id == section.id" (click)="selectSection(section)" [class.core-selected-item]="selected?.id == section.id"
[class.item-dimmed]="section.visible === 0 || section.uservisible === false" detail="false" role="menuitem" [class.item-dimmed]="section.visible === 0 || section.uservisible === false" detail="false"
[attr.aria-hidden]="section.uservisible === false" button> [attr.aria-hidden]="section.uservisible === false" button>
<ion-icon name="fas-folder" slot="start"></ion-icon> <ion-icon name="fas-folder" slot="start" aria-hidden="true"></ion-icon>
<ion-label> <ion-label>
<h2><core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> <h2><core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
</core-format-text></h2> </core-format-text></h2>
<core-progress-bar *ngIf="section.progress >= 0" [progress]="section.progress"></core-progress-bar> <core-progress-bar *ngIf="section.progress >= 0" [progress]="section.progress"></core-progress-bar>
<ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap"> <ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible !== false"
class="ion-text-wrap">
{{ 'core.course.hiddenfromstudents' | translate }} {{ 'core.course.hiddenfromstudents' | translate }}
</ion-badge> </ion-badge>
<ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap"> <ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible === false"
class="ion-text-wrap">
{{ 'core.notavailable' | translate }} {{ 'core.notavailable' | translate }}
</ion-badge> </ion-badge>
<ion-badge color="secondary" *ngIf="section.availabilityinfo" class="ion-text-wrap"> <ion-badge color="secondary" *ngIf="section.availabilityinfo" class="ion-text-wrap">

View File

@ -11,7 +11,7 @@
[class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)" [class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)"
[class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner"> [class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner">
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)" <p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
class="core-course-additional-info"> class="core-course-additional-info" aria-hidden="true">
<span *ngIf="course.categoryname" class="core-course-category"> <span *ngIf="course.categoryname" class="core-course-category">
<core-format-text [text]="course.categoryname"></core-format-text> <core-format-text [text]="course.categoryname"></core-format-text>
</span> </span>
@ -24,7 +24,10 @@
</span> </span>
</p> </p>
<h2> <h2>
<ion-icon name="fas-star" *ngIf="course.isfavourite"></ion-icon> <ion-icon name="fas-star" *ngIf="course.isfavourite" [attr.aria-label]="'core.courses.favourite' | translate">
</ion-icon>
<span class="sr-only" *ngIf="course.isfavourite">{{ 'core.courses.aria:favourite' | translate }}</span>
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text> <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text>
</h2> </h2>
</ion-label> </ion-label>
@ -50,9 +53,10 @@
</ion-button> </ion-button>
</div> </div>
</ion-item> </ion-item>
<ion-item *ngIf="showAll && course.progress! >= 0 && course.completionusertracked !== false" <ion-item *ngIf="showAll && course.progress! >= 0 && course.completionusertracked !== false" lines="none">
lines="none"> <ion-label>
<ion-label><core-progress-bar [progress]="course.progress"></core-progress-bar></ion-label> <core-progress-bar [progress]="course.progress"></core-progress-bar>
</ion-label>
</ion-item> </ion-item>
<ng-content></ng-content> <ng-content></ng-content>
</ion-card> </ion-card>

View File

@ -20,7 +20,7 @@
type="password" type="password"
placeholder="{{ 'core.courses.password' | translate }}" placeholder="{{ 'core.courses.password' | translate }}"
[(ngModel)]="password" [(ngModel)]="password"
[core-auto-focus] [autofocus]="true"
[clearOnEdit]="false"> [clearOnEdit]="false">
</ion-input> </ion-input>
</core-show-password> </core-show-password>

View File

@ -1,6 +1,9 @@
{ {
"addtofavourites": "Star this course", "addtofavourites": "Star this course",
"allowguests": "This course allows guest users to enter", "allowguests": "This course allows guest users to enter",
"aria:coursename": "Course name",
"aria:courseprogress": "Course progress:",
"aria:favourite": "Course is starred",
"availablecourses": "Available courses", "availablecourses": "Available courses",
"cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.", "cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.",
"categories": "Course categories", "categories": "Course categories",
@ -13,6 +16,7 @@
"errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.", "errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.",
"errorsearching": "An error occurred while searching.", "errorsearching": "An error occurred while searching.",
"errorselfenrol": "An error occurred while self enrolling.", "errorselfenrol": "An error occurred while self enrolling.",
"favourite": "Starred course",
"filtermycourses": "Filter my courses", "filtermycourses": "Filter my courses",
"frontpage": "Front page", "frontpage": "Front page",
"hidecourse": "Remove from view", "hidecourse": "Remove from view",

View File

@ -31,7 +31,7 @@
<ion-item> <ion-item>
<ion-label></ion-label> <ion-label></ion-label>
<ion-input type="text" name="value" placeholder="{{ 'core.login.usernameoremail' | translate }}" <ion-input type="text" name="value" placeholder="{{ 'core.login.usernameoremail' | translate }}"
formControlName="value" autocapitalize="none" autocorrect="off" [core-auto-focus]="autoFocus"> formControlName="value" autocapitalize="none" autocorrect="off" [autofocus]="autoFocus">
</ion-input> </ion-input>
</ion-item> </ion-item>
<ion-button type="submit" class="ion-margin" expand="block" [disabled]="!myForm.valid"> <ion-button type="submit" class="ion-margin" expand="block" [disabled]="!myForm.valid">

View File

@ -27,7 +27,7 @@
<h2>{{ 'core.login.siteaddress' | translate }}</h2> <h2>{{ 'core.login.siteaddress' | translate }}</h2>
</ion-label> </ion-label>
<ion-input name="url" type="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}" <ion-input name="url" type="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}"
formControlName="siteUrl" [core-auto-focus]="showKeyboard && !showScanQR"> formControlName="siteUrl" [autofocus]="showKeyboard && !showScanQR">
</ion-input> </ion-input>
</ion-item> </ion-item>
</ng-container> </ng-container>
@ -37,7 +37,7 @@
<h2>{{ 'core.login.siteaddress' | translate }}</h2> <h2>{{ 'core.login.siteaddress' | translate }}</h2>
</ion-label> </ion-label>
<ion-input name="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}" formControlName="siteUrl" <ion-input name="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}" formControlName="siteUrl"
[core-auto-focus]="showKeyboard && !showScanQR" (ionChange)="searchSite($event, siteForm.value.siteUrl)"> [autofocus]="showKeyboard && !showScanQR" (ionChange)="searchSite($event, siteForm.value.siteUrl)">
</ion-input> </ion-input>
</ion-item> </ion-item>
@ -49,7 +49,7 @@
</ion-item> </ion-item>
<ion-item button *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)" <ion-item button *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)"
[attr.aria-label]="'core.login.connect' | translate" detail-push class="core-login-entered-site"> [attr.aria-label]="'core.login.connect' | translate" detail-push class="core-login-entered-site">
<ion-thumbnail slot="start"> <ion-thumbnail slot="start" aria-hidden="true">
<ion-icon name="fas-pen"></ion-icon> <ion-icon name="fas-pen"></ion-icon>
</ion-thumbnail> </ion-thumbnail>
<ion-label> <ion-label>
@ -101,7 +101,8 @@
<ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl"> <ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl">
<div class="ion-text-center ion-padding ion-margin-top">{{ 'core.login.or' | translate }}</div> <div class="ion-text-center ion-padding ion-margin-top">{{ 'core.login.or' | translate }}</div>
<ion-button expand="block" color="light" class="ion-margin" lines="none" (click)="showInstructionsAndScanQR()"> <ion-button expand="block" color="light" class="ion-margin" lines="none" (click)="showInstructionsAndScanQR()"
aria-haspopup="true">
<ion-icon slot="start" name="fas-qrcode" aria-hidden="true"></ion-icon> <ion-icon slot="start" name="fas-qrcode" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.scanqr' | translate }}</ion-label> <ion-label>{{ 'core.scanqr' | translate }}</ion-label>
</ion-button> </ion-button>
@ -109,7 +110,8 @@
<!-- Help. --> <!-- Help. -->
<ion-list lines="none" class="ion-margin-top"> <ion-list lines="none" class="ion-margin-top">
<ion-item button class="ion-text-center ion-text-wrap core-login-need-help" (click)="showHelp()" detail="false"> <ion-item button class="ion-text-center ion-text-wrap core-login-need-help" (click)="showHelp()" detail="false"
aria-haspopup="true">
<ion-label>{{ 'core.needhelp' | translate }}</ion-label> <ion-label>{{ 'core.needhelp' | translate }}</ion-label>
</ion-item> </ion-item>
</ion-list> </ion-list>

View File

@ -13,7 +13,6 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<!-- @todo -->
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<core-tabs-outlet *ngIf="tabs.length > 0" [selectedIndex]="selectedTab" [hideUntil]="loaded" [tabs]="tabs"> <core-tabs-outlet *ngIf="tabs.length > 0" [selectedIndex]="selectedTab" [hideUntil]="loaded" [tabs]="tabs">
</core-tabs-outlet> </core-tabs-outlet>

View File

@ -14,7 +14,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreMainMenuHomeDelegate } from '../home-delegate';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../mainmenu-delegate'; import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../mainmenu-delegate';
/** /**
@ -29,27 +28,14 @@ export class CoreMainMenuHomeHandlerService implements CoreMainMenuHandler {
priority = 1100; priority = 1100;
/** /**
* Check if the handler is enabled on a site level. * @inheritdoc
*
* @return Whether or not the handler is enabled on a site level.
*/ */
isEnabled(): Promise<boolean> { async isEnabled(): Promise<boolean> {
return this.isEnabledForSite(); return true;
} }
/** /**
* Check if the handler is enabled on a certain site. * @inheritdoc
*
* @return Whether or not the handler is enabled on a site level.
*/
async isEnabledForSite(): Promise<boolean> {
return CoreMainMenuHomeDelegate.getHandlers().length > 0;
}
/**
* Returns the data needed to render the handler.
*
* @return Data needed to render the handler.
*/ */
getDisplayData(): CoreMainMenuHandlerData { getDisplayData(): CoreMainMenuHandlerData {
return { return {

View File

@ -3,7 +3,7 @@
<ion-item> <ion-item>
<ion-label></ion-label> <ion-label></ion-label>
<ion-input type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" <ion-input type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder"
[autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [autofocus]="autoFocus"
[disabled]="disabled" role="searchbox" (ionFocus)="focus($event)"> [disabled]="disabled" role="searchbox" (ionFocus)="focus($event)">
</ion-input> </ion-input>
<ion-button slot="end" fill="clear" type="submit" size="small" [attr.aria-label]="searchLabel" <ion-button slot="end" fill="clear" type="submit" size="small" [attr.aria-label]="searchLabel"

View File

@ -655,7 +655,7 @@ export class CoreSitesProvider {
* @return Release number or empty. * @return Release number or empty.
*/ */
getReleaseNumber(rawRelease: string): string { getReleaseNumber(rawRelease: string): string {
const matches = rawRelease.match(/^\d(\.\d(\.\d+)?)?/); const matches = rawRelease.match(/^\d+(\.\d+(\.\d+)?)?/);
if (matches) { if (matches) {
return matches[0]; return matches[0];
} }

View File

@ -0,0 +1,21 @@
// Text for accessibility, hidden from the view.
.sr-only, .accesshide {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.sr-only-focusable:active, .sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
overflow: visible;
clip: auto;
white-space: normal;
}

View File

@ -436,14 +436,6 @@ ion-button.core-button-select {
background-color: var(--text-hightlight-background-color); background-color: var(--text-hightlight-background-color);
} }
// Text for accessibility, hidden from the view.
.accesshide {
position: absolute;
left: -10000px;
font-weight: normal;
font-size: 1em;
}
// Monospaced font. // Monospaced font.
.core-monospaced { .core-monospaced {
font-family: Andale Mono,Monaco,Courier New,DejaVu Sans Mono,monospace; font-family: Andale Mono,Monaco,Courier New,DejaVu Sans Mono,monospace;

View File

@ -23,6 +23,9 @@
@import "./components/rubrics.scss"; @import "./components/rubrics.scss";
@import "./components/mod-label.scss"; @import "./components/mod-label.scss";
/* Some styles from 3rd party libraries. */
@import "./bootstrap.scss";
/* Core CSS required for Ionic components to work properly */ /* Core CSS required for Ionic components to work properly */
@import "~@ionic/angular/css/core.css"; @import "~@ionic/angular/css/core.css";