Merge pull request #2706 from dpalou/MOBILE-3649

Mobile 3649
main
Dani Palou 2021-03-16 10:33:43 +01:00 committed by GitHub
commit f1c6e5dd28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
130 changed files with 1355 additions and 302 deletions

View File

@ -8,7 +8,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event)">
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="badgeLoaded">

View File

@ -101,7 +101,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
*
* @param refresher Refresher.
*/
async refreshBadges(refresher?: CustomEvent<IonRefresher>): Promise<void> {
async refreshBadges(refresher?: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(Promise.all([
AddonBadges.invalidateUserBadges(this.courseId, this.userId),
]));
@ -110,7 +110,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
this.fetchIssuedBadge(),
]));
refresher?.detail.complete();
refresher?.complete();
}
}

View File

@ -8,7 +8,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="core-loading-center">

View File

@ -250,7 +250,7 @@ export class AddonBlogEntriesPage implements OnInit {
*
* @param refresher Refresher instance.
*/
refresh(refresher?: CustomEvent<IonRefresher>): void {
refresh(refresher?: IonRefresher): void {
const promises = this.entries.map((entry) =>
CoreComments.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog'));
@ -269,7 +269,7 @@ export class AddonBlogEntriesPage implements OnInit {
CoreUtils.allPromises(promises).finally(() => {
this.fetchEntries(true).finally(() => {
if (refresher) {
refresher?.detail.complete();
refresher?.complete();
}
});
});

View File

@ -21,7 +21,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -431,13 +431,13 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
* @param done Function to call when done.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void): Promise<void> {
async doRefresh(refresher?: IonRefresher, done?: () => void): Promise<void> {
if (!this.loaded) {
return;
}
await this.refreshData(true).finally(() => {
refresher?.detail.complete();
refresher?.complete();
done && done();
});
}

View File

@ -7,7 +7,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -363,7 +363,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
*
* @param refresher Refresher.
*/
refreshData(refresher?: CustomEvent<IonRefresher>): void {
refreshData(refresher?: IonRefresher): void {
const promises = [
AddonCalendar.invalidateAccessInformation(this.courseId),
AddonCalendar.invalidateAllowedEventTypes(this.courseId),
@ -384,7 +384,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
Promise.all(promises).finally(() => {
this.fetchData().finally(() => {
refresher?.detail.complete();
refresher?.complete();
});
});
}

View File

@ -31,7 +31,7 @@
</core-context-menu>
</core-navbar-buttons>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!eventLoaded" (ionRefresh)="doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!eventLoaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="eventLoaded">

View File

@ -402,13 +402,13 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
* @param showErrors Whether to show sync errors to the user.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors= false): Promise<void> {
async doRefresh(refresher?: IonRefresher, done?: () => void, showErrors= false): Promise<void> {
if (!this.eventLoaded) {
return;
}
await this.refreshEvent(true, showErrors).finally(() => {
refresher?.detail.complete();
refresher?.complete();
done && done();
});
}

View File

@ -26,7 +26,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -265,13 +265,13 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
* @param showErrors Whether to show sync errors to the user.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors?: boolean): Promise<void> {
async doRefresh(refresher?: IonRefresher, done?: () => void, showErrors?: boolean): Promise<void> {
if (!this.loaded) {
return;
}
await this.refreshData(true, showErrors).finally(() => {
refresher?.detail.complete();
refresher?.complete();
done && done();
});
}

View File

@ -21,7 +21,7 @@
</ion-header>
<ion-content>
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!eventsLoaded" (ionRefresh)="doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!eventsLoaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="eventsLoaded">

View File

@ -549,13 +549,13 @@ export class AddonCalendarListPage implements OnInit, OnDestroy {
* @param showErrors Whether to show sync errors to the user.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors?: boolean): Promise<void> {
async doRefresh(refresher?: IonRefresher, done?: () => void, showErrors?: boolean): Promise<void> {
if (!this.eventsLoaded) {
return;
}
await this.refreshEvents(true, showErrors).finally(() => {
refresher?.detail.complete();
refresher?.complete();
done && done();
});
}

View File

@ -7,7 +7,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!devicesLoaded" (ionRefresh)="refreshDevices($event)">
<ion-refresher slot="fixed" [disabled]="!devicesLoaded" (ionRefresh)="refreshDevices($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="devicesLoaded">

View File

@ -106,13 +106,13 @@ export class AddonMessageOutputAirnotifierDevicesPage implements OnInit, OnDestr
*
* @param refresher Refresher.
*/
async refreshDevices(refresher: CustomEvent<IonRefresher>): Promise<void> {
async refreshDevices(refresher: IonRefresher): Promise<void> {
try {
await CoreUtils.ignoreErrors(AddonMessageOutputAirnotifier.invalidateUserDevices());
await this.fetchDevices();
} finally {
refresher?.detail.complete();
refresher?.complete();
}
}

View File

@ -9,7 +9,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -122,7 +122,7 @@ export class AddonMessagesConversationInfoComponent implements OnInit {
* @param refresher Refresher.
* @return Promise resolved when done.
*/
async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> {
async refreshData(refresher?: IonRefresher): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonMessages.invalidateConversation(this.conversationId));
@ -131,7 +131,7 @@ export class AddonMessagesConversationInfoComponent implements OnInit {
await Promise.all(promises);
await this.fetchData().finally(() => {
refresher?.detail.complete();
refresher?.complete();
});
}

View File

@ -13,7 +13,7 @@
</ion-header>
<ion-content>
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -128,7 +128,7 @@ export class AddonMessagesContacts35Page implements OnInit, OnDestroy {
* @param refresher Refresher.
* @return Promise resolved when done.
*/
async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> {
async refreshData(refresher?: IonRefresher): Promise<void> {
try {
if (this.searchString) {
// User has searched, update the search.
@ -139,7 +139,7 @@ export class AddonMessagesContacts35Page implements OnInit, OnDestroy {
await this.fetchData();
}
} finally {
refresher?.detail.complete();
refresher?.complete();
}
}

View File

@ -21,7 +21,7 @@
<!-- Contacts tab. -->
<core-tab [title]="'addon.messages.contacts' | translate" (ionSelect)="selectTab('confirmed')">
<ng-template>
<ion-refresher slot="fixed" [disabled]="!confirmedLoaded" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!confirmedLoaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="confirmedLoaded" class="core-loading-center">
@ -57,7 +57,7 @@
<!-- Requests tab. -->
<core-tab [title]="'addon.messages.requests' | translate" (ionSelect)="selectTab('requests')" [badge]="requestsBadge">
<ng-template>
<ion-refresher slot="fixed" [disabled]="!requestsLoaded" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!requestsLoaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="requestsLoaded" class="core-loading-center">

View File

@ -185,7 +185,7 @@ export class AddonMessagesContactsPage implements OnInit, OnDestroy {
* @param refresher Refresher.
* @return Promise resolved when done.
*/
async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> {
async refreshData(refresher?: IonRefresher): Promise<void> {
try {
if (this.selected == 'confirmed') {
// No need to invalidate contacts, we always try to get the latest.
@ -198,7 +198,7 @@ export class AddonMessagesContactsPage implements OnInit, OnDestroy {
await this.requestsFetchData(true);
}
} finally {
refresher?.detail.complete();
refresher?.complete();
}
}

View File

@ -13,7 +13,7 @@
</ion-header>
<ion-content>
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -168,7 +168,7 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
* @param refreshUnreadCounts Whteher to refresh unread counts.
* @return Promise resolved when done.
*/
async refreshData(refresher?: CustomEvent<IonRefresher>, refreshUnreadCounts: boolean = true): Promise<void> {
async refreshData(refresher?: IonRefresher, refreshUnreadCounts: boolean = true): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonMessages.invalidateDiscussionsCache(this.siteId));
@ -178,7 +178,7 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
await CoreUtils.allPromises(promises).finally(() => this.fetchData().finally(() => {
if (refresher) {
refresher?.detail.complete();
refresher?.complete();
}
}));
}

View File

@ -19,7 +19,7 @@
</ion-header>
<ion-content class="core-expand-max">
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!loaded || !currentListEl" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!loaded || !currentListEl" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -700,7 +700,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* @param refreshUnreadCounts Whether to refresh unread counts.
* @return Promise resolved when done.
*/
async refreshData(refresher?: CustomEvent<IonRefresher>, refreshUnreadCounts: boolean = true): Promise<void> {
async refreshData(refresher?: IonRefresher, refreshUnreadCounts: boolean = true): Promise<void> {
// Don't invalidate conversations and so, they always try to get latest data.
try {
await AddonMessages.invalidateContactRequestsCountCache(this.siteId);
@ -709,7 +709,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
await this.fetchData(refreshUnreadCounts);
} finally {
if (refresher) {
refresher?.detail.complete();
refresher?.complete();
}
}
}

View File

@ -7,7 +7,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!preferencesLoaded" (ionRefresh)="refreshPreferences($event)">
<ion-refresher slot="fixed" [disabled]="!preferencesLoaded" (ionRefresh)="refreshPreferences($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="preferencesLoaded">

View File

@ -251,10 +251,10 @@ export class AddonMessagesSettingsPage implements OnInit, OnDestroy {
*
* @param refresher Refresher.
*/
refreshPreferences(refresher?: CustomEvent<IonRefresher>): void {
refreshPreferences(refresher?: IonRefresher): void {
AddonMessages.invalidateMessagePreferences().finally(() => {
this.fetchPreferences().finally(() => {
refresher?.detail.complete();
refresher?.complete();
});
});
}

View File

@ -35,7 +35,7 @@
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120"
contextLevel="module" [contextInstanceId]="module!.id" [courseId]="courseId" (click)="expandDescription($event)">
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId" (click)="expandDescription($event)">
</core-format-text>
</ion-label>
</ion-item>
@ -136,7 +136,7 @@
<!-- If it's a student, display his submission. -->
<addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId"
[moduleId]="module!.id">
[moduleId]="module.id">
</addon-mod-assign-submission>
</core-loading>

View File

@ -120,7 +120,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
(data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment submitted, check completion.
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
// Reload data since it can have offline data now.
this.showLoadingAndRefresh(true, false);
@ -140,7 +140,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
try {
await AddonModAssign.logView(this.assign!.id, this.assign!.name);
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors. Just don't check Module completion.
}
@ -164,11 +164,11 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
if (this.assign && (this.description || this.assign.introattachments)) {
CoreTextUtils.viewText(Translate.instant('core.description'), this.description || '', {
component: this.component,
componentId: this.module!.id,
componentId: this.module.id,
files: this.assign.introattachments,
filter: true,
contextLevel: 'module',
instanceId: this.module!.id,
instanceId: this.module.id,
courseId: this.courseId,
});
}
@ -186,7 +186,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
// Get assignment data.
try {
this.assign = await AddonModAssign.getAssignment(this.courseId!, this.module!.id);
this.assign = await AddonModAssign.getAssignment(this.courseId, this.module.id);
this.dataRetrieved.emit(this.assign);
this.description = this.assign.intro;
@ -200,7 +200,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
this.hasOffline = await AddonModAssignOffline.hasAssignOfflineData(this.assign.id);
// Get assignment submissions.
const submissions = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.module!.id });
const submissions = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.module.id });
const time = CoreTimeUtils.timestamp();
this.canViewAllSubmissions = submissions.canviewsubmissions;
@ -244,7 +244,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
try {
// Check if the user can view their own submission.
await AddonModAssign.getSubmissionStatus(this.assign.id, { cmId: this.module!.id });
await AddonModAssign.getSubmissionStatus(this.assign.id, { cmId: this.module.id });
this.canViewOwnSubmission = true;
} catch (error) {
this.canViewOwnSubmission = false;
@ -269,7 +269,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
const submissionStatus = await AddonModAssign.getSubmissionStatus(this.assign!.id, {
groupId: this.group,
cmId: this.module!.id,
cmId: this.module.id,
});
this.summary = submissionStatus.gradingsummary;
@ -345,7 +345,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModAssign.invalidateAssignmentData(this.courseId!));
promises.push(AddonModAssign.invalidateAssignmentData(this.courseId));
if (this.assign) {
promises.push(AddonModAssign.invalidateAllSubmissionData(this.assign.id));

View File

@ -14,7 +14,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -14,7 +14,7 @@
<ion-content>
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!loaded || !submissions.loaded" (ionRefresh)="refreshList($event)">
<ion-refresher slot="fixed" [disabled]="!loaded || !submissions.loaded" (ionRefresh)="refreshList($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded && submissions.loaded">

View File

@ -328,9 +328,9 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
*
* @param refresher Refresher.
*/
refreshList(refresher?: CustomEvent<IonRefresher>): void {
refreshList(refresher?: IonRefresher): void {
this.refreshAllData(true).finally(() => {
refresher?.detail.complete();
refresher?.complete();
});
}

View File

@ -19,7 +19,7 @@
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">

View File

@ -155,9 +155,9 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
*
* @param refresher Refresher.
*/
refreshSubmission(refresher?: CustomEvent<IonRefresher>): void {
refreshSubmission(refresher?: IonRefresher): void {
this.refreshAllData().finally(() => {
refresher?.detail.complete();
refresher?.complete();
});
}

View File

@ -24,7 +24,7 @@
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId"></core-course-module-description>
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description>
<ion-card class="core-warning-card" *ngIf="warning">
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
@ -38,7 +38,7 @@
</core-navigation-bar>
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module"
[contextInstanceId]="module?.id" [courseId]="courseId"></core-format-text>
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
<div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0">
<strong>{{ 'core.tag.tags' | translate }}: </strong>
<core-tag-list [tags]="tags"></core-tag-list>

View File

@ -87,7 +87,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
const modal = await ModalController.create({
component: AddonModBookTocComponent,
componentProps: {
moduleId: this.module!.id,
moduleId: this.module.id,
chapters: this.chapters,
selected: this.currentChapter,
courseId: this.courseId,
@ -129,7 +129,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
* @return Resolved when done.
*/
protected invalidateContent(): Promise<void> {
return AddonModBook.invalidateContent(this.module!.id, this.courseId!);
return AddonModBook.invalidateContent(this.module.id, this.courseId);
}
/**
@ -143,7 +143,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
let downloadResult: CoreCourseResourceDownloadResult | undefined;
// Try to get the book data. Ignore errors since this WS isn't available in some Moodle versions.
promises.push(CoreUtils.ignoreErrors(AddonModBook.getBook(this.courseId!, this.module!.id))
promises.push(CoreUtils.ignoreErrors(AddonModBook.getBook(this.courseId, this.module.id))
.then((book) => {
if (!book) {
return;
@ -169,8 +169,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
try {
await Promise.all(promises);
this.contentsMap = AddonModBook.getContentsMap(this.module!.contents);
this.chapters = AddonModBook.getTocList(this.module!.contents);
this.contentsMap = AddonModBook.getContentsMap(this.module.contents);
this.chapters = AddonModBook.getTocList(this.module.contents);
if (typeof this.currentChapter == 'undefined' && typeof this.initialChapterId != 'undefined' && this.chapters) {
// Initial chapter set. Validate that the chapter exists.
@ -211,7 +211,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
this.content?.scrollToTop();
try {
const content = await AddonModBook.getChapterContent(this.contentsMap, chapterId, this.module!.id);
const content = await AddonModBook.getChapterContent(this.contentsMap, chapterId, this.module.id);
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
@ -228,14 +228,14 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
await CoreUtils.ignoreErrors(AddonModBook.logView(
this.module!.instance!,
this.module.instance!,
logChapterId ? chapterId : undefined,
this.module!.name,
this.module.name,
));
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
if (!this.nextChapter) {
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_book.errorchapter', true);

View File

@ -13,7 +13,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -26,7 +26,7 @@
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<ion-list *ngIf="subfolder && (subfolder!.files.length + subfolder!.folders.length > 0)">

View File

@ -55,7 +55,7 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo
this.canGetFolder = AddonModFolder.isGetFolderWSAvailable();
if (this.subfolder) {
this.description = this.folderInstance ? this.folderInstance.intro : this.module!.description;
this.description = this.folderInstance ? this.folderInstance.intro : this.module.description;
this.loaded = true;
this.refreshIcon = CoreConstants.ICON_REFRESH;
@ -67,8 +67,8 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo
await this.loadContent();
try {
await AddonModFolder.logView(this.module!.instance!, this.module!.name);
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
await AddonModFolder.logView(this.module.instance!, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
@ -84,7 +84,7 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
await AddonModFolder.invalidateContent(this.module!.id, this.courseId!);
await AddonModFolder.invalidateContent(this.module.id, this.courseId);
}
/**
@ -96,22 +96,22 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo
protected async fetchContent(refresh = false): Promise<void> {
try {
if (this.canGetFolder) {
this.folderInstance = await AddonModFolder.getFolder(this.courseId!, this.module!.id);
await CoreCourse.loadModuleContents(this.module!, this.courseId, undefined, false, refresh);
this.folderInstance = await AddonModFolder.getFolder(this.courseId, this.module.id);
await CoreCourse.loadModuleContents(this.module, this.courseId, undefined, false, refresh);
} else {
const module = await CoreCourse.getModule(this.module!.id, this.courseId);
const module = await CoreCourse.getModule(this.module.id, this.courseId);
if (!module.contents.length && this.module!.contents.length && !CoreApp.isOnline()) {
if (!module.contents.length && this.module.contents.length && !CoreApp.isOnline()) {
// The contents might be empty due to a cached data. Use the old ones.
module.contents = this.module!.contents;
module.contents = this.module.contents;
}
this.module = module;
}
this.dataRetrieved.emit(this.folderInstance || this.module);
this.description = this.folderInstance ? this.folderInstance.intro : this.module!.description;
this.subfolder = AddonModFolderHelper.formatContents(this.module!.contents);
this.description = this.folderInstance ? this.folderInstance.intro : this.module.description;
this.subfolder = AddonModFolderHelper.formatContents(this.module.contents);
} finally {
this.fillContextMenu(refresh);
}

View File

@ -15,7 +15,7 @@
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="subfolder || !activityComponent?.loaded"
(ionRefresh)="activityComponent?.doRefresh($event)">
(ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -37,7 +37,7 @@
<!-- Content. -->
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!discussions.loaded" (ionRefresh)="doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!discussions.loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -135,7 +135,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
this.eventReceived.bind(this, false),
);
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module!.id) {
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) {
AddonModForum.invalidateDiscussionsList(this.forum!.id).finally(() => {
if (data.discussionId) {
// Discussion changed, search it in the list of discussions.
@ -198,7 +198,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
AddonModForum.instance
.logView(this.forum.id, this.forum.name)
.then(async () => {
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
return;
}),
@ -324,7 +324,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
promises.push(
AddonModForum.instance
.getAccessInformation(this.forum.id, { cmId: this.module!.id })
.getAccessInformation(this.forum.id, { cmId: this.module.id })
.then(async accessInfo => {
// Disallow adding discussions if cut-off date is reached and the user has not the
// capability to override it.
@ -341,7 +341,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
// Use the canAddDiscussion WS to check if the user can pin discussions.
promises.push(
AddonModForum.instance
.canAddDiscussionToAll(this.forum.id, { cmId: this.module!.id })
.canAddDiscussionToAll(this.forum.id, { cmId: this.module.id })
.then(async response => {
this.canPin = !!response.canpindiscussions;
@ -525,7 +525,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModForum.invalidateForumData(this.courseId!));
promises.push(AddonModForum.invalidateForumData(this.courseId));
if (this.forum) {
promises.push(AddonModForum.invalidateDiscussionsList(this.forum.id));
@ -546,7 +546,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
* @return Promise resolved when done.
*/
protected sync(): Promise<AddonModForumSyncResult> {
return AddonModForumPrefetchHandler.sync(this.module!, this.courseId!);
return AddonModForumPrefetchHandler.sync(this.module, this.courseId);
}
/**
@ -582,7 +582,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
isNewDiscussion: boolean,
data: AddonModForumNewDiscussionData | AddonModForumReplyDiscussionData,
): void {
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module?.id) {
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) {
this.showLoadingAndRefresh(false).finally(() => {
// If it's a new discussion in tablet mode, try to open it.
if (isNewDiscussion && CoreScreen.isTablet) {
@ -606,7 +606,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
});
// Check completion since it could be configured to complete once the user adds a new discussion or replies.
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
}
@ -668,7 +668,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
componentProps: {
discussion,
forumId: this.forum!.id,
cmId: this.module!.id,
cmId: this.module.id,
},
event,
});
@ -733,7 +733,7 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager<Discussio
getItemQueryParams(discussion: DiscussionItem): Params {
return {
courseId: this.component.courseId,
cmId: this.component.module!.id,
cmId: this.component.module.id,
forumId: this.component.forum!.id,
...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}),
};

View File

@ -61,7 +61,7 @@
</core-context-menu>
</core-navbar-buttons>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -21,7 +21,7 @@ import { CoreRatingOffline } from '@features/rating/services/rating-offline';
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
import { CoreUser } from '@features/user/services/user';
import { CanLeave } from '@guards/can-leave';
import { IonContent } from '@ionic/angular';
import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreApp } from '@services/app';
import { CoreNavigator } from '@services/navigator';
import { CoreScreen } from '@services/screen';
@ -612,10 +612,10 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: any, done?: () => void, showErrors: boolean = false): Promise<void> {
async doRefresh(refresher?: IonRefresher | null, done?: () => void, showErrors: boolean = false): Promise<void> {
if (this.discussionLoaded) {
await this.refreshPosts(true, showErrors).finally(() => {
refresher && refresher.complete();
refresher?.complete();
done && done();
});
}
@ -651,7 +651,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
* @param type Sort type.
* @return Promised resolved when done.
*/
changeSort(type: SortType): Promise<any> {
changeSort(type: SortType): Promise<void> {
this.discussionLoaded = false;
this.sort = type;
CoreSites.getCurrentSite()!.setLocalSiteConfig('AddonModForumDiscussionSort', this.sort);

View File

@ -36,7 +36,7 @@
<div class="addon-mod-imscp-container">
<core-navigation-bar [previous]="previousItem" [next]="nextItem" (action)="loadItem($event)" [info]="description"
[title]="'core.description' | translate" [component]="component" [componentId]="componentId" contextLevel="module"
[contextInstanceId]="module?.id" [courseId]="courseId">
[contextInstanceId]="module.id" [courseId]="courseId">
</core-navigation-bar>
<core-iframe [src]="src"></core-iframe>
</div>

View File

@ -59,8 +59,8 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
await this.loadContent();
try {
await AddonModImscp.logView(this.module!.instance!, this.module!.name);
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
await AddonModImscp.logView(this.module.instance!, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
@ -72,7 +72,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
await AddonModImscp.invalidateContent(this.module!.id, this.courseId!);
await AddonModImscp.invalidateContent(this.module.id, this.courseId);
}
/**
@ -85,7 +85,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
let downloadResult: CoreCourseResourceDownloadResult;
const promises: Promise<void>[] = [];
promises.push(AddonModImscp.getImscp(this.courseId!, this.module!.id).then((imscp) => {
promises.push(AddonModImscp.getImscp(this.courseId, this.module.id).then((imscp) => {
this.description = imscp.intro;
this.dataRetrieved.emit(imscp);
@ -101,7 +101,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
try {
await Promise.all(promises);
this.items = AddonModImscp.createItemList(this.module!.contents);
this.items = AddonModImscp.createItemList(this.module.contents);
if (this.items.length && typeof this.currentItem == 'undefined') {
this.currentItem = this.items[0].href;
@ -129,7 +129,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
* @return Promise resolved when done.
*/
async loadItem(itemId?: string): Promise<void> {
const src = await AddonModImscp.getIframeSrc(this.module!, itemId);
const src = await AddonModImscp.getIframeSrc(this.module, itemId);
this.currentItem = itemId;
this.previousItem = itemId ? AddonModImscp.getPreviousItem(this.items, itemId) : '';
this.nextItem = itemId ? AddonModImscp.getNextItem(this.items, itemId) : '';

View File

@ -32,7 +32,7 @@
<core-tab [title]="'addon.mod_lesson.preview' | translate" (ionSelect)="indexSelected()">
<ng-template>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<!-- Prevent access messages. Only show the first one. -->

View File

@ -146,7 +146,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
let lessonReady = true;
this.askPassword = false;
this.lesson = await AddonModLesson.getLesson(this.courseId!, this.module!.id);
this.lesson = await AddonModLesson.getLesson(this.courseId, this.module.id);
this.dataRetrieved.emit(this.lesson);
this.description = this.lesson.intro; // Show description only if intro is present.
@ -156,7 +156,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
await this.syncActivity(showErrors);
}
this.accessInfo = await AddonModLesson.getAccessInformation(this.lesson.id, { cmId: this.module!.id });
this.accessInfo = await AddonModLesson.getAccessInformation(this.lesson.id, { cmId: this.module.id });
this.canManage = this.accessInfo.canmanage;
this.canViewReports = this.accessInfo.canviewreports;
this.preventReasons = [];
@ -227,7 +227,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
}
const promises: Promise<unknown>[] = [];
const options = { cmId: this.module!.id };
const options = { cmId: this.module.id };
// Check if there is offline data.
promises.push(AddonModLessonSync.hasDataToSync(this.lesson.id, this.accessInfo.attemptscount).then((hasData) => {
@ -293,7 +293,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
protected hasSyncSucceed(result: AddonModLessonSyncResult): boolean {
if (result.updated || this.dataSent) {
// Check completion status if something was sent.
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
this.dataSent = false;
@ -339,7 +339,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
protected async invalidateContent(): Promise<void> {
const promises: Promise<unknown>[] = [];
promises.push(AddonModLesson.invalidateLessonData(this.courseId!));
promises.push(AddonModLesson.invalidateLessonData(this.courseId));
if (this.lesson) {
promises.push(AddonModLesson.invalidateAccessInformation(this.lesson.id));
@ -394,7 +394,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
AddonModLesson.logViewLesson(this.lesson.id, this.password, this.lesson.name),
);
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
/**
@ -414,7 +414,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
if (this.hasOffline) {
if (continueLast) {
pageId = await AddonModLesson.getLastPageSeen(this.lesson.id, this.accessInfo.attemptscount, {
cmId: this.module!.id,
cmId: this.module.id,
});
} else {
pageId = this.accessInfo.firstpageid;
@ -589,7 +589,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
this.showSpinner = true;
try {
await AddonModLessonPrefetchHandler.prefetch(this.module!, this.courseId, true);
await AddonModLessonPrefetchHandler.prefetch(this.module, this.courseId, true);
// Success downloading, open lesson.
this.playLesson(continueLast);
@ -661,8 +661,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
// The user sent data to server, but not in the sync process. Check if we need to fetch data.
await CoreUtils.ignoreErrors(AddonModLessonSync.prefetchAfterUpdate(
AddonModLessonPrefetchHandler.instance,
this.module!,
this.courseId!,
this.module,
this.courseId,
));
}
@ -677,7 +677,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
*/
protected async validatePassword(password: string): Promise<void> {
try {
this.lesson = await AddonModLesson.getLessonWithPassword(this.lesson!.id, { password, cmId: this.module!.id });
this.lesson = await AddonModLesson.getLessonWithPassword(this.lesson!.id, { password, cmId: this.module.id });
this.password = password;
} catch (error) {

View File

@ -13,7 +13,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -7,7 +7,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -98,9 +98,9 @@ export class AddonModLessonUserRetakePage implements OnInit {
*
* @param refresher Refresher.
*/
doRefresh(refresher: CustomEvent<IonRefresher>): void {
doRefresh(refresher: IonRefresher): void {
this.refreshData().finally(() => {
refresher?.detail.complete();
refresher?.complete();
});
}

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { AddonModLtiIndexComponent } from './index';
@NgModule({
declarations: [
AddonModLtiIndexComponent,
],
imports: [
CoreSharedModule,
CoreCourseComponentsModule,
],
exports: [
AddonModLtiIndexComponent,
],
})
export class AddonModLtiComponentsModule {}

View File

@ -0,0 +1,31 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
[href]="externalUrl" iconAction="fas-external-link-alt">
</core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
(action)="expandDescription()" iconAction="fas-arrow-right">
</core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
iconAction="far-newspaper" (action)="gotoBlog()">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && isOnline" [priority]="700" [content]="'core.refresh' | translate"
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<core-course-module-description *ngIf="lti && lti.showdescriptionlaunch" [description]="description" [component]="component"
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<div class="ion-padding">
<ion-button expand="block" (click)="launch()">
<ion-icon name="fas-external-link-alt" slot="start"></ion-icon>
{{ 'addon.mod_lti.launchactivity' | translate }}
</ion-button>
</div>
</core-loading>

View File

@ -0,0 +1,90 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Optional, OnInit } from '@angular/core';
import { IonContent } from '@ionic/angular';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { AddonModLti, AddonModLtiLti, AddonModLtiProvider } from '../../services/lti';
import { AddonModLtiHelper } from '../../services/lti-helper';
/**
* Component that displays an LTI entry page.
*/
@Component({
selector: 'addon-mod-lti-index',
templateUrl: 'addon-mod-lti-index.html',
})
export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit {
component = AddonModLtiProvider.COMPONENT;
moduleName = 'lti';
lti?: AddonModLtiLti; // The LTI object.
protected fetchContentDefaultError = 'addon.mod_lti.errorgetlti';
constructor(
protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModLtiIndexComponent', content, courseContentsPage);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.loadContent();
}
/**
* @inheritdoc
*/
protected async fetchContent(refresh: boolean = false): Promise<void> {
try {
this.lti = await AddonModLti.getLti(this.courseId, this.module.id);
this.description = this.lti.intro;
this.dataRetrieved.emit(this.lti);
} finally {
this.fillContextMenu(refresh);
}
}
/**
* @inheritdoc
*/
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModLti.invalidateLti(this.courseId));
if (this.lti) {
promises.push(AddonModLti.invalidateLtiLaunchData(this.lti.id));
}
await Promise.all(promises);
}
/**
* Launch the LTI.
*/
launch(): void {
AddonModLtiHelper.getDataAndLaunch(this.courseId, this.module, this.lti);
}
}

View File

@ -0,0 +1,6 @@
{
"errorgetlti": "Error getting module data.",
"errorinvalidlaunchurl": "The launch URL is not valid.",
"launchactivity": "Launch the activity",
"modulenameplural": "External tools"
}

View File

@ -0,0 +1,38 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSharedModule } from '@/core/shared.module';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AddonModLtiComponentsModule } from './components/components.module';
import { AddonModLtiIndexPage } from './pages/index/index.page';
const routes: Routes = [
{
path: ':courseId/:cmId',
component: AddonModLtiIndexPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonModLtiComponentsModule,
],
declarations: [
AddonModLtiIndexPage,
],
})
export class AddonModLtiLazyModule {}

View File

@ -0,0 +1,60 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { AddonModLtiComponentsModule } from './components/components.module';
import { AddonModLtiIndexLinkHandler } from './services/handlers/index-link';
import { AddonModLtiListLinkHandler } from './services/handlers/list-link';
import { AddonModLtiModuleHandler, AddonModLtiModuleHandlerService } from './services/handlers/module';
import { AddonModLtiPrefetchHandler } from './services/handlers/prefetch';
import { AddonModLtiProvider } from './services/lti';
import { AddonModLtiHelperProvider } from './services/lti-helper';
export const ADDON_MOD_LTI_SERVICES: Type<unknown>[] = [
AddonModLtiProvider,
AddonModLtiHelperProvider,
];
const routes: Routes = [
{
path: AddonModLtiModuleHandlerService.PAGE_NAME,
loadChildren: () => import('./lti-lazy.module').then(m => m.AddonModLtiLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
AddonModLtiComponentsModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
CoreCourseModuleDelegate.registerHandler(AddonModLtiModuleHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModLtiIndexLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModLtiListLinkHandler.instance);
CoreCourseModulePrefetchDelegate.registerHandler(AddonModLtiPrefetchHandler.instance);
},
},
],
})
export class AddonModLtiModule {}

View File

@ -0,0 +1,22 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-lti-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-lti-index>
</ion-content>

View File

@ -0,0 +1,31 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild } from '@angular/core';
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
import { AddonModLtiIndexComponent } from '../../components/index/index';
/**
* Page that displays an LTI.
*/
@Component({
selector: 'page-addon-mod-lti-index',
templateUrl: 'index.html',
})
export class AddonModLtiIndexPage extends CoreCourseModuleMainActivityPage<AddonModLtiIndexComponent> {
@ViewChild(AddonModLtiIndexComponent) activityComponent?: AddonModLtiIndexComponent;
}

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { makeSingleton } from '@singletons';
/**
* Handler to treat links to LTI.
*/
@Injectable({ providedIn: 'root' })
export class AddonModLtiIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
name = 'AddonModLtiIndexLinkHandlerService';
constructor() {
super('AddonModLti', 'lti', 'l');
}
}
export const AddonModLtiIndexLinkHandler = makeSingleton(AddonModLtiIndexLinkHandlerService);

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
import { makeSingleton } from '@singletons';
/**
* Handler to treat links to LTI list page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModLtiListLinkHandlerService extends CoreContentLinksModuleListHandler {
name = 'AddonModLtiListLinkHandler';
constructor() {
super('AddonModLti', 'lti');
}
}
export const AddonModLtiListLinkHandler = makeSingleton(AddonModLtiListLinkHandlerService);

View File

@ -0,0 +1,145 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Type } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { CoreConstants } from '@/core/constants';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreApp } from '@services/app';
import { CoreFilepool } from '@services/filepool';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonModLtiHelper } from '../lti-helper';
import { AddonModLti, AddonModLtiProvider } from '../lti';
import { AddonModLtiIndexComponent } from '../../components/index';
/**
* Handler to support LTI modules.
*/
@Injectable({ providedIn: 'root' })
export class AddonModLtiModuleHandlerService implements CoreCourseModuleHandler {
static readonly PAGE_NAME = 'mod_lti';
name = 'AddonModLti';
modName = 'lti';
supportedFeatures = {
[CoreConstants.FEATURE_GROUPS]: false,
[CoreConstants.FEATURE_GROUPINGS]: false,
[CoreConstants.FEATURE_MOD_INTRO]: true,
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
[CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
};
constructor(protected sanitizer: DomSanitizer) {}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
getData(
module: CoreCourseAnyModuleData,
courseId: number,
): CoreCourseModuleHandlerData {
const data: CoreCourseModuleHandlerData = {
icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
title: module.name,
class: 'addon-mod_lti-handler',
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
options = options || {};
options.params = options.params || {};
Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.navigateToSitePath(AddonModLtiModuleHandlerService.PAGE_NAME + routeParams, options);
},
buttons: [{
icon: 'link',
label: 'addon.mod_lti.launchactivity',
action: (event: Event, module: CoreCourseModule, courseId: number): void => {
// Launch the LTI.
AddonModLtiHelper.getDataAndLaunch(courseId, module);
},
}],
};
// Handle custom icons.
CoreUtils.ignoreErrors(this.loadCustomIcon(module, courseId, data));
return data;
}
/**
* Load the custom icon.
*
* @param module Module.
* @param courseId Course ID.
* @param data Handler data.
* @return Promise resolved when done.
*/
protected async loadCustomIcon(
module: CoreCourseAnyModuleData,
courseId: number,
handlerData: CoreCourseModuleHandlerData,
): Promise<void> {
const lti = await AddonModLti.getLti(courseId, module.id);
const icon = lti.secureicon || lti.icon;
if (!icon) {
return;
}
const siteId = CoreSites.getCurrentSiteId();
try {
await CoreFilepool.downloadUrl(siteId, icon, false, AddonModLtiProvider.COMPONENT, module.id);
// Get the internal URL.
const url = await CoreFilepool.getSrcByUrl(siteId, icon, AddonModLtiProvider.COMPONENT, module.id);
handlerData.icon = this.sanitizer.bypassSecurityTrustUrl(url);
} catch {
// Error downloading. If we're online we'll set the online url.
if (CoreApp.isOnline()) {
handlerData.icon = this.sanitizer.bypassSecurityTrustUrl(icon);
}
}
}
/**
* @inheritdoc
*/
async getMainComponent(): Promise<Type<unknown> | undefined> {
return AddonModLtiIndexComponent;
}
}
export const AddonModLtiModuleHandler = makeSingleton(AddonModLtiModuleHandlerService);

View File

@ -0,0 +1,62 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
import { CoreCourseAnyModuleData } from '@features/course/services/course';
import { makeSingleton } from '@singletons';
import { AddonModLti, AddonModLtiProvider } from '../lti';
/**
* Handler to prefetch LTIs. LTIs cannot be prefetched, but the handler will be used to invalidate some data on course PTR.
*/
@Injectable({ providedIn: 'root' })
export class AddonModLtiPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
name = 'AddonModLti';
modName = 'lti';
component = AddonModLtiProvider.COMPONENT;
/**
* @inheritdoc
*/
async download(): Promise<void> {
return;
}
/**
* @inheritdoc
*/
invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
return AddonModLti.invalidateLti(courseId);
}
/**
* @inheritdoc
*/
async isDownloadable(): Promise<boolean> {
return false; // LTIs aren't downloadable.
}
/**
* @inheritdoc
*/
async prefetch(): Promise<void> {
return;
}
}
export const AddonModLtiPrefetchHandler = makeSingleton(AddonModLtiPrefetchHandlerService);

View File

@ -0,0 +1,125 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { makeSingleton, Platform } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { AddonModLti, AddonModLtiLti } from './lti';
/**
* Service that provides some helper functions for LTI.
*/
@Injectable({ providedIn: 'root' })
export class AddonModLtiHelperProvider {
protected pendingCheckCompletion: {[moduleId: string]: {courseId: number; module: CoreCourseModule}} = {};
constructor() {
Platform.resume.subscribe(() => {
// User went back to the app, check pending completions.
for (const moduleId in this.pendingCheckCompletion) {
const data = this.pendingCheckCompletion[moduleId];
CoreCourse.checkModuleCompletion(data.courseId, data.module.completiondata);
}
});
// Clear pending completion on logout.
CoreEvents.on(CoreEvents.LOGOUT, () => {
this.pendingCheckCompletion = {};
});
}
/**
* Get needed data and launch the LTI.
*
* @param courseId Course ID.
* @param module Module.
* @param lti LTI instance. If not provided it will be obtained.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async getDataAndLaunch(courseId: number, module: CoreCourseModule, lti?: AddonModLtiLti, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId();
const modal = await CoreDomUtils.showModalLoading();
try {
const openInBrowser = await AddonModLti.isOpenInAppBrowserDisabled(siteId);
if (openInBrowser) {
const site = await CoreSites.getSite(siteId);
// The view event is triggered by the browser, mark the module as pending to check completion.
this.pendingCheckCompletion[module.id] = {
courseId,
module,
};
return site.openInBrowserWithAutoLogin(module.url!);
}
// Open in app.
if (!lti) {
lti = await AddonModLti.getLti(courseId, module.id);
}
const launchData = await AddonModLti.getLtiLaunchData(lti.id);
// "View" LTI without blocking the UI.
this.logViewAndCheckCompletion(courseId, module, lti.id, lti.name, siteId);
// Launch LTI.
return AddonModLti.launch(launchData.endpoint, launchData.parameters);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_lti.errorgetlti', true);
} finally {
modal.dismiss();
}
}
/**
* Report the LTI as being viewed and check completion.
*
* @param courseId Course ID.
* @param module Module.
* @param ltiId LTI id.
* @param name Name of the lti.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async logViewAndCheckCompletion(
courseId: number,
module: CoreCourseModule,
ltiId: number,
name?: string,
siteId?: string,
): Promise<void> {
try {
await AddonModLti.logView(ltiId, name, siteId);
CoreCourse.checkModuleCompletion(courseId, module.completiondata);
} catch (error) {
// Ignore errors.
}
}
}
export const AddonModLtiHelper = makeSingleton(AddonModLtiHelperProvider);

View File

@ -0,0 +1,344 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreApp } from '@services/app';
import { CoreFile } from '@services/file';
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
const ROOT_CACHE_KEY = 'mmaModLti:';
const LAUNCHER_FILE_NAME = 'lti_launcher.html';
/**
* Service that provides some features for LTI.
*/
@Injectable({ providedIn: 'root' })
export class AddonModLtiProvider {
static readonly COMPONENT = 'mmaModLti';
/**
* Delete launcher.
*
* @return Promise resolved when the launcher file is deleted.
*/
deleteLauncher(): Promise<void> {
return CoreFile.removeFile(LAUNCHER_FILE_NAME);
}
/**
* Generates a launcher file.
*
* @param url Launch URL.
* @param params Launch params.
* @return Promise resolved with the file URL.
*/
async generateLauncher(url: string, params: AddonModLtiParam[]): Promise<string> {
if (!CoreFile.isAvailable()) {
return url;
}
// Generate a form with the params.
let text = `<form action="${url}" name="ltiLaunchForm" method="post" encType="application/x-www-form-urlencoded">\n`;
params.forEach((p) => {
if (p.name == 'ext_submit') {
text += ' <input type="submit"';
} else {
text += ' <input type="hidden" name="' + CoreTextUtils.escapeHTML(p.name) + '"';
}
text += ' value="' + CoreTextUtils.escapeHTML(p.value) + '"/>\n';
});
text += '</form>\n';
// Add an in-line script to automatically submit the form.
text += '<script type="text/javascript"> \n' +
' window.onload = function() { \n' +
' document.ltiLaunchForm.submit(); \n' +
' }; \n' +
'</script> \n';
const entry = await CoreFile.writeFile(LAUNCHER_FILE_NAME, text);
return entry.toURL();
}
/**
* Get a LTI.
*
* @param courseId Course ID.
* @param cmId Course module ID.
* @param options Other options.
* @return Promise resolved when the LTI is retrieved.
*/
async getLti(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModLtiLti> {
const params: AddonModLtiGetLtisByCoursesWSParams = {
courseids: [courseId],
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getLtiCacheKey(courseId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
component: AddonModLtiProvider.COMPONENT,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const site = await CoreSites.getSite(options.siteId);
const response = await site.read<AddonModLtiGetLtisByCoursesWSResponse>('mod_lti_get_ltis_by_courses', params, preSets);
const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId);
if (currentLti) {
return currentLti;
}
throw new CoreError('Activity not found.');
}
/**
* Get cache key for LTI data WS calls.
*
* @param courseId Course ID.
* @return Cache key.
*/
protected getLtiCacheKey(courseId: number): string {
return ROOT_CACHE_KEY + 'lti:' + courseId;
}
/**
* Get a LTI launch data.
*
* @param id LTI id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the launch data is retrieved.
*/
async getLtiLaunchData(id: number, siteId?: string): Promise<AddonModLtiGetToolLaunchDataWSResponse> {
const params: AddonModLtiGetToolLaunchDataWSParams = {
toolid: id,
};
// Try to avoid using cache since the "nonce" parameter is set to a timestamp.
const preSets: CoreSiteWSPreSets = {
getFromCache: false,
saveToCache: true,
emergencyCache: true,
cacheKey: this.getLtiLaunchDataCacheKey(id),
};
const site = await CoreSites.getSite(siteId);
return site.read<AddonModLtiGetToolLaunchDataWSResponse>('mod_lti_get_tool_launch_data', params, preSets);
}
/**
* Get cache key for LTI launch data WS calls.
*
* @param id LTI id.
* @return Cache key.
*/
protected getLtiLaunchDataCacheKey(id: number): string {
return `${ROOT_CACHE_KEY}launch:${id}`;
}
/**
* Invalidates LTI data.
*
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateLti(courseId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getLtiCacheKey(courseId));
}
/**
* Invalidates options.
*
* @param id LTI id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateLtiLaunchData(id: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getLtiLaunchDataCacheKey(id));
}
/**
* Check if open in InAppBrowser is disabled.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether it's disabled.
*/
async isOpenInAppBrowserDisabled(siteId?: string): Promise<boolean> {
const site = await CoreSites.getSite(siteId);
return this.isOpenInAppBrowserDisabledInSite(site);
}
/**
* Check if open in InAppBrowser is disabled.
*
* @param site Site. If not defined, current site.
* @return Whether it's disabled.
*/
isOpenInAppBrowserDisabledInSite(site?: CoreSite): boolean {
site = site || CoreSites.getCurrentSite();
return !!site?.isFeatureDisabled('CoreCourseModuleDelegate_AddonModLti:openInAppBrowser');
}
/**
* Launch LTI.
*
* @param url Launch URL.
* @param params Launch params.
* @return Promise resolved when the WS call is successful.
*/
async launch(url: string, params: AddonModLtiParam[]): Promise<void> {
if (!CoreUrlUtils.isHttpURL(url)) {
throw Translate.instant('addon.mod_lti.errorinvalidlaunchurl');
}
// Generate launcher and open it.
const launcherUrl = await this.generateLauncher(url, params);
if (CoreApp.isMobile()) {
CoreUtils.openInApp(launcherUrl);
} else {
// In desktop open in browser, we found some cases where inapp caused JS issues.
CoreUtils.openInBrowser(launcherUrl);
}
}
/**
* Report the LTI as being viewed.
*
* @param id LTI id.
* @param name Name of the lti.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful.
*/
logView(id: number, name?: string, siteId?: string): Promise<any> {
const params: AddonModLtiViewLtiWSParams = {
ltiid: id,
};
return CoreCourseLogHelper.logSingle(
'mod_lti_view_lti',
params,
AddonModLtiProvider.COMPONENT,
id,
name,
'lti',
{},
siteId,
);
}
}
export const AddonModLti = makeSingleton(AddonModLtiProvider);
/**
* Params of mod_lti_get_ltis_by_courses WS.
*/
export type AddonModLtiGetLtisByCoursesWSParams = {
courseids?: number[]; // Array of course ids.
};
/**
* Data returned by mod_lti_get_ltis_by_courses WS.
*/
export type AddonModLtiGetLtisByCoursesWSResponse = {
ltis: AddonModLtiLti[];
warnings?: CoreWSExternalWarning[];
};
/**
* LTI returned by mod_lti_get_ltis_by_courses.
*/
export type AddonModLtiLti = {
id: number; // External tool id.
coursemodule: number; // Course module id.
course: number; // Course id.
name: string; // LTI name.
intro?: string; // The LTI intro.
introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
introfiles?: CoreWSExternalFile[]; // @since 3.2.
timecreated?: number; // Time of creation.
timemodified?: number; // Time of last modification.
typeid?: number; // Type id.
toolurl?: string; // Tool url.
securetoolurl?: string; // Secure tool url.
instructorchoicesendname?: string; // Instructor choice send name.
instructorchoicesendemailaddr?: number; // Instructor choice send mail address.
instructorchoiceallowroster?: number; // Instructor choice allow roster.
instructorchoiceallowsetting?: number; // Instructor choice allow setting.
instructorcustomparameters?: string; // Instructor custom parameters.
instructorchoiceacceptgrades?: number; // Instructor choice accept grades.
grade?: number; // Enable grades.
launchcontainer?: number; // Launch container mode.
resourcekey?: string; // Resource key.
password?: string; // Shared secret.
debuglaunch?: number; // Debug launch.
showtitlelaunch?: number; // Show title launch.
showdescriptionlaunch?: number; // Show description launch.
servicesalt?: string; // Service salt.
icon?: string; // Alternative icon URL.
secureicon?: string; // Secure icon URL.
section?: number; // Course section id.
visible?: number; // Visible.
groupmode?: number; // Group mode.
groupingid?: number; // Group id.
};
/**
* Params of mod_lti_get_tool_launch_data WS.
*/
export type AddonModLtiGetToolLaunchDataWSParams = {
toolid: number; // External tool instance id.
};
/**
* Data returned by mod_lti_get_tool_launch_data WS.
*/
export type AddonModLtiGetToolLaunchDataWSResponse = {
endpoint: string; // Endpoint URL.
parameters: AddonModLtiParam[];
warnings?: CoreWSExternalWarning[];
};
/**
* Param to send to the LTI.
*/
export type AddonModLtiParam = {
name: string; // Parameter name.
value: string; // Parameter value.
};
/**
* Params of mod_lti_view_lti WS.
*/
export type AddonModLtiViewLtiWSParams = {
ltiid: number; // Lti instance id.
};

View File

@ -25,6 +25,7 @@ import { AddonModPageModule } from './page/page.module';
import { AddonModQuizModule } from './quiz/quiz.module';
import { AddonModResourceModule } from './resource/resource.module';
import { AddonModUrlModule } from './url/url.module';
import { AddonModLtiModule } from './lti/lti.module';
@NgModule({
declarations: [],
@ -40,6 +41,7 @@ import { AddonModUrlModule } from './url/url.module';
AddonModResourceModule,
AddonModFolderModule,
AddonModImscpModule,
AddonModLtiModule,
],
providers: [],
exports: [],

View File

@ -26,7 +26,7 @@
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component"
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<ion-card class="core-warning-card" *ngIf="warning">
@ -36,7 +36,7 @@
<div class="ion-padding">
<core-format-text [component]="component" [componentId]="componentId" [text]="contents" contextLevel="module"
[contextInstanceId]="module?.id" [courseId]="courseId">
[contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
<p class="ion-padding-bottom addon-mod_page-timemodified" *ngIf="displayTimemodified && timemodified">

View File

@ -59,8 +59,8 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
await this.loadContent();
try {
await AddonModPage.logView(this.module!.instance!, this.module!.name);
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
await AddonModPage.logView(this.module.instance!, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
@ -72,7 +72,7 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
await AddonModPage.invalidateContent(this.module!.id, this.courseId!);
await AddonModPage.invalidateContent(this.module.id, this.courseId);
}
/**
@ -92,9 +92,9 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
// Get the module to get the latest title and description. Data should've been updated in download.
if (this.canGetPage) {
getPagePromise = AddonModPage.getPageData(this.courseId!, this.module!.id);
getPagePromise = AddonModPage.getPageData(this.courseId, this.module.id);
} else {
getPagePromise = CoreCourse.getModule(this.module!.id, this.courseId!);
getPagePromise = CoreCourse.getModule(this.module.id, this.courseId);
}
promises.push(getPagePromise.then((page) => {
@ -133,7 +133,7 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
}));
// Get the page HTML.
promises.push(AddonModPageHelper.getPageHtml(this.module!.contents, this.module!.id).then((content) => {
promises.push(AddonModPageHelper.getPageHtml(this.module.contents, this.module.id).then((content) => {
this.contents = content;
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';

View File

@ -13,7 +13,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -28,7 +28,7 @@
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<!-- Access rules description messages. -->
@ -117,7 +117,7 @@
<ion-label>
<h3 class="item-heading">{{ 'addon.mod_quiz.comment' | translate }}</h3>
<p><core-format-text [component]="component" [componentId]="componentId" [text]="gradebookFeedback"
contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text></p>
</ion-label>
</ion-item>
@ -125,7 +125,7 @@
<ion-label>
<h3 class="item-heading">{{ 'addon.mod_quiz.overallfeedback' | translate }}</h3>
<p><core-format-text [component]="component" [componentId]="componentId" [text]="overallFeedback"
contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text></p>
</ion-label>
</ion-item>

View File

@ -129,7 +129,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
try {
await AddonModQuiz.logViewQuiz(this.quiz.id, this.quiz.name);
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
@ -162,7 +162,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
this.showStatusSpinner = true;
try {
await AddonModQuizPrefetchHandler.prefetch(this.module!, this.courseId, true);
await AddonModQuizPrefetchHandler.prefetch(this.module, this.courseId, true);
// Success downloading, open quiz.
this.openQuiz();
@ -190,7 +190,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
try {
// First get the quiz instance.
const quiz = await AddonModQuiz.getQuiz(this.courseId!, this.module!.id);
const quiz = await AddonModQuiz.getQuiz(this.courseId, this.module.id);
this.gradeMethodReadable = AddonModQuiz.getQuizGradeMethod(quiz.grademethod);
this.now = Date.now();
@ -231,7 +231,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
}
// Get quiz access info.
this.quizAccessInfo = await AddonModQuiz.getQuizAccessInformation(quiz.id, { cmId: this.module!.id });
this.quizAccessInfo = await AddonModQuiz.getQuizAccessInformation(quiz.id, { cmId: this.module.id });
this.showReviewColumn = this.quizAccessInfo.canreviewmyattempts;
this.accessRules = this.quizAccessInfo.accessrules;
@ -242,7 +242,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
}
// Get question types in the quiz.
const types = await AddonModQuiz.getQuizRequiredQtypes(quiz.id, { cmId: this.module!.id });
const types = await AddonModQuiz.getQuizRequiredQtypes(quiz.id, { cmId: this.module.id });
this.unsupportedQuestions = AddonModQuiz.getUnsupportedQuestions(types);
this.hasSupportedQuestions = !!types.find((type) => type != 'random' && this.unsupportedQuestions.indexOf(type) == -1);
@ -265,10 +265,10 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
protected async getAttempts(quiz: AddonModQuizQuizData): Promise<void> {
// Get access information of last attempt (it also works if no attempts made).
this.attemptAccessInfo = await AddonModQuiz.getAttemptAccessInformation(quiz.id, 0, { cmId: this.module!.id });
this.attemptAccessInfo = await AddonModQuiz.getAttemptAccessInformation(quiz.id, 0, { cmId: this.module.id });
// Get attempts.
const attempts = await AddonModQuiz.getUserAttempts(quiz.id, { cmId: this.module!.id });
const attempts = await AddonModQuiz.getUserAttempts(quiz.id, { cmId: this.module.id });
this.attempts = await this.treatAttempts(quiz, attempts);
@ -386,7 +386,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
if (quiz.showFeedbackColumn) {
// Get the quiz overall feedback.
const response = await AddonModQuiz.getFeedbackForGrade(quiz.id, this.gradebookData.grade, {
cmId: this.module!.id,
cmId: this.module.id,
});
this.overallFeedback = response.feedbacktext;
@ -404,14 +404,14 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
}
// If we go to auto review it means an attempt was finished. Check completion status.
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
// Verify that user can see the review.
const attemptId = this.autoReview.attemptId;
if (this.quizAccessInfo?.canreviewmyattempts) {
try {
await AddonModQuiz.getAttemptReview(attemptId, { page: -1, cmId: this.module!.id });
await AddonModQuiz.getAttemptReview(attemptId, { page: -1, cmId: this.module.id });
await CoreNavigator.navigate(`review/${attemptId}`);
} catch {
@ -429,7 +429,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
protected hasSyncSucceed(result: AddonModQuizSyncResult): boolean {
if (result.attemptFinished) {
// An attempt was finished, check completion status.
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
// If the sync call isn't rejected it means the sync was successful.
@ -488,7 +488,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModQuiz.invalidateQuizData(this.courseId!));
promises.push(AddonModQuiz.invalidateQuizData(this.courseId));
if (this.quiz) {
promises.push(AddonModQuiz.invalidateUserAttemptsForUser(this.quiz.id));
@ -497,7 +497,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
promises.push(AddonModQuiz.invalidateAttemptAccessInformation(this.quiz.id));
promises.push(AddonModQuiz.invalidateCombinedReviewOptionsForUser(this.quiz.id));
promises.push(AddonModQuiz.invalidateUserBestGradeForUser(this.quiz.id));
promises.push(AddonModQuiz.invalidateGradeFromGradebook(this.courseId!));
promises.push(AddonModQuiz.invalidateGradeFromGradebook(this.courseId));
}
await Promise.all(promises);
@ -536,7 +536,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
CoreNavigator.navigate('player', {
params: {
moduleUrl: this.module?.url,
moduleUrl: this.module.url,
},
});
}
@ -594,7 +594,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
}
// Get combined review options.
promises.push(AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module!.id }).then((options) => {
promises.push(AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module.id }).then((options) => {
this.options = options;
return;
@ -633,11 +633,11 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
* @return Promise resolved when done.
*/
protected async getQuizGrade(quiz: AddonModQuizQuizData): Promise<void> {
this.bestGrade = await AddonModQuiz.getUserBestGrade(quiz.id, { cmId: this.module!.id });
this.bestGrade = await AddonModQuiz.getUserBestGrade(quiz.id, { cmId: this.module.id });
try {
// Get gradebook grade.
const data = await AddonModQuiz.getGradeFromGradebook(this.courseId!, this.module!.id);
const data = await AddonModQuiz.getGradeFromGradebook(this.courseId, this.module.id);
if (data) {
this.gradebookData = {

View File

@ -14,7 +14,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -22,7 +22,7 @@
<core-course-module-description *ngIf="mode != 'iframe' && (mode != 'embedded' || displayDescription)"
[description]="description" [component]="component" [componentId]="componentId" contextLevel="module"
[contextInstanceId]="module!.id" [courseId]="courseId">
[contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<ion-card class="core-warning-card" *ngIf="warning">

View File

@ -64,8 +64,8 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
await this.loadContent();
try {
await AddonModResource.logView(this.module!.instance!, this.module!.name);
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
await AddonModResource.logView(this.module.instance!, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
@ -77,7 +77,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
return AddonModResource.invalidateContent(this.module!.id, this.courseId!);
return AddonModResource.invalidateContent(this.module.id, this.courseId);
}
/**
@ -88,9 +88,9 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
*/
protected async fetchContent(refresh?: boolean): Promise<void> {
// Load module contents if needed. Passing refresh is needed to force reloading contents.
await CoreCourse.loadModuleContents(this.module!, this.courseId, undefined, false, refresh);
await CoreCourse.loadModuleContents(this.module, this.courseId, undefined, false, refresh);
if (!this.module!.contents || !this.module!.contents.length) {
if (!this.module.contents || !this.module.contents.length) {
throw new CoreError(Translate.instant('core.filenotfound'));
}
@ -99,11 +99,11 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
// Get the resource instance to get the latest name/description and to know if it's embedded.
if (this.canGetResource) {
resource = await CoreUtils.ignoreErrors(AddonModResource.getResourceData(this.courseId!, this.module!.id));
resource = await CoreUtils.ignoreErrors(AddonModResource.getResourceData(this.courseId, this.module.id));
this.description = resource?.intro || '';
options = resource?.displayoptions ? CoreTextUtils.unserialize(resource.displayoptions) : {};
} else {
resource = await CoreUtils.ignoreErrors(CoreCourse.getModule(this.module!.id, this.courseId));
resource = await CoreUtils.ignoreErrors(CoreCourse.getModule(this.module.id, this.courseId));
this.description = resource?.description || '';
options = resource?.customdata ? CoreTextUtils.unserialize(CoreTextUtils.parseJSON(resource.customdata)) : {};
}
@ -114,9 +114,9 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
this.dataRetrieved.emit(resource);
}
if (AddonModResourceHelper.isDisplayedInIframe(this.module!)) {
if (AddonModResourceHelper.isDisplayedInIframe(this.module)) {
const downloadResult = await this.downloadResourceIfNeeded(refresh, true);
const src = await AddonModResourceHelper.getIframeSrc(this.module!);
const src = await AddonModResourceHelper.getIframeSrc(this.module);
this.mode = 'iframe';
if (this.src && src.toString() == this.src.toString()) {
@ -137,11 +137,11 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
return;
}
if (resource && 'display' in resource && AddonModResourceHelper.isDisplayedEmbedded(this.module!, resource.display)) {
if (resource && 'display' in resource && AddonModResourceHelper.isDisplayedEmbedded(this.module, resource.display)) {
this.mode = 'embedded';
this.warning = '';
this.contentText = await AddonModResourceHelper.getEmbeddedHtml(this.module!, this.courseId!);
this.contentText = await AddonModResourceHelper.getEmbeddedHtml(this.module, this.courseId);
this.mode = this.contentText.length > 0 ? 'embedded' : 'external';
} else {
this.mode = 'external';
@ -158,20 +158,20 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
* @return Promise resolved when done.
*/
async open(): Promise<void> {
let downloadable = await CoreCourseModulePrefetchDelegate.isModuleDownloadable(this.module!, this.courseId!);
let downloadable = await CoreCourseModulePrefetchDelegate.isModuleDownloadable(this.module, this.courseId);
if (downloadable) {
// Check if the main file is downloadle.
// This isn't done in "isDownloadable" to prevent extra WS calls in the course page.
downloadable = await AddonModResourceHelper.isMainFileDownloadable(this.module!);
downloadable = await AddonModResourceHelper.isMainFileDownloadable(this.module);
if (downloadable) {
return AddonModResourceHelper.openModuleFile(this.module!, this.courseId!);
return AddonModResourceHelper.openModuleFile(this.module, this.courseId);
}
}
// The resource cannot be downloaded, open the activity in browser.
await CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(this.module!.url!);
await CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(this.module.url!);
}
}

View File

@ -16,7 +16,7 @@
<ion-content>
<ion-refresher slot="fixed"
[disabled]="!activityComponent?.loaded || activityComponent?.mode != 'external'"
(ionRefresh)="activityComponent?.doRefresh($event)">
(ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -16,7 +16,7 @@
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component"
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module!.id" [courseId]="courseId">
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<div *ngIf="shouldIframe || (shouldEmbed && isOther)" class="addon-mod_url-embedded-url">

View File

@ -75,7 +75,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
await AddonModUrl.invalidateContent(this.module!.id, this.courseId!);
await AddonModUrl.invalidateContent(this.module.id, this.courseId);
}
/**
@ -90,7 +90,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
throw null;
}
// Fetch the module data.
const url = await AddonModUrl.getUrl(this.courseId!, this.module!.id);
const url = await AddonModUrl.getUrl(this.courseId, this.module.id);
this.name = url.name;
this.description = url.intro;
@ -102,17 +102,17 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
}
// Try to load module contents, it's needed to get the URL with parameters.
await CoreCourse.loadModuleContents(this.module!, this.courseId, undefined, false, refresh, undefined, 'url');
await CoreCourse.loadModuleContents(this.module, this.courseId, undefined, false, refresh, undefined, 'url');
// Always use the URL from the module because it already includes the parameters.
this.url = this.module!.contents[0] && this.module!.contents[0].fileurl ? this.module!.contents[0].fileurl : undefined;
this.url = this.module.contents[0] && this.module.contents[0].fileurl ? this.module.contents[0].fileurl : undefined;
await this.calculateDisplayOptions(url);
} catch {
// Fallback in case is not prefetched or not available.
const mod =
await CoreCourse.getModule(this.module!.id, this.courseId, undefined, false, false, undefined, 'url');
await CoreCourse.getModule(this.module.id, this.courseId, undefined, false, false, undefined, 'url');
this.name = mod.name;
this.description = mod.description;
@ -167,8 +167,8 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
*/
protected async logView(): Promise<void> {
try {
await AddonModUrl.logView(this.module!.instance!, this.module!.name);
CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
await AddonModUrl.logView(this.module.instance!, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}

View File

@ -13,7 +13,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -7,7 +7,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!notificationsLoaded" (ionRefresh)="refreshNotifications($event)">
<ion-refresher slot="fixed" [disabled]="!notificationsLoaded" (ionRefresh)="refreshNotifications($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="notificationsLoaded">

View File

@ -174,13 +174,13 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
* @param refresher Refresher.
* @return Promise<any> Promise resolved when done.
*/
async refreshNotifications(refresher?: CustomEvent<IonRefresher>): Promise<void> {
async refreshNotifications(refresher?: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(AddonNotifications.invalidateNotificationsList());
try {
await this.fetchNotifications(true);
} finally {
refresher?.detail.complete();
refresher?.complete();
}
}

View File

@ -16,7 +16,8 @@
</core-context-menu>
</core-navbar-buttons>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!preferencesLoaded || !notifPrefsEnabled" (ionRefresh)="refreshPreferences($event)">
<ion-refresher slot="fixed" [disabled]="!preferencesLoaded || !notifPrefsEnabled"
(ionRefresh)="refreshPreferences($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="preferencesLoaded">

View File

@ -179,13 +179,13 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy {
*
* @param refresher Refresher.
*/
async refreshPreferences(refresher?: CustomEvent<IonRefresher>): Promise<void> {
async refreshPreferences(refresher?: IonRefresher): Promise<void> {
try {
await CoreUtils.ignoreErrors(AddonNotifications.invalidateNotificationPreferences());
await this.fetchPreferences();
} finally {
refresher?.detail.complete();
refresher?.complete();
}
}

View File

@ -8,7 +8,7 @@
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!filesLoaded || (!showPrivateFiles && !showSiteFiles)"
(ionRefresh)="refreshData($event)">
(ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -119,9 +119,9 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
*
* @param refresher Refresher.
*/
refreshData(event?: CustomEvent<IonRefresher>): void {
refreshData(event?: IonRefresher): void {
this.refreshFiles().finally(() => {
event?.detail.complete();
event?.complete();
});
}

View File

@ -71,10 +71,10 @@ export abstract class CoreBlockBaseComponent implements OnInit {
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors: boolean = false): Promise<void> {
async doRefresh(refresher?: IonRefresher, done?: () => void, showErrors: boolean = false): Promise<void> {
if (this.loaded) {
return this.refreshContent(showErrors).finally(() => {
refresher?.detail.complete();
refresher?.complete();
done && done();
});
}

View File

@ -140,7 +140,7 @@ export class CoreBlockComponent implements OnInit, OnDestroy, DoCheck {
* @return Promise resolved when done.
*/
async doRefresh(
refresher?: CustomEvent<IonRefresher>,
refresher?: IonRefresher,
done?: () => void,
showErrors: boolean = false,
): Promise<void> {

View File

@ -26,7 +26,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!commentsLoaded" (ionRefresh)="refreshComments(false, $event)">
<ion-refresher slot="fixed" [disabled]="!commentsLoaded" (ionRefresh)="refreshComments(false, $event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="commentsLoaded">

View File

@ -205,7 +205,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
* @param refresher Refresher.
* @return Resolved when done.
*/
async refreshComments(showErrors: boolean, refresher?: CustomEvent<IonRefresher>): Promise<void> {
async refreshComments(showErrors: boolean, refresher?: IonRefresher): Promise<void> {
this.commentsLoaded = false;
this.refreshIcon = CoreConstants.ICON_LOADING;
this.syncIcon = CoreConstants.ICON_LOADING;
@ -219,7 +219,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
try {
await this.fetchComments(true, showErrors);
} finally {
refresher?.detail.complete();
refresher?.complete();
}
}
}

View File

@ -132,7 +132,7 @@ import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module';
// @todo import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module';
import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module';
import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module';
// @todo import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.module';
import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.module';
import { ADDON_MOD_PAGE_SERVICES } from '@addons/mod/page/page.module';
import { ADDON_MOD_QUIZ_SERVICES } from '@addons/mod/quiz/quiz.module';
import { ADDON_MOD_RESOURCE_SERVICES } from '@addons/mod/resource/resource.module';
@ -297,7 +297,7 @@ export class CoreCompileProvider {
// @todo ...ADDON_MOD_H5P_ACTIVITY_SERVICES,
...ADDON_MOD_IMSCP_SERVICES,
...ADDON_MOD_LESSON_SERVICES,
// @todo ...ADDON_MOD_LTI_SERVICES,
...ADDON_MOD_LTI_SERVICES,
...ADDON_MOD_PAGE_SERVICES,
...ADDON_MOD_QUIZ_SERVICES,
...ADDON_MOD_RESOURCE_SERVICES,

View File

@ -50,8 +50,8 @@ export type CoreCourseResourceDownloadResult = {
})
export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, CoreCourseModuleMainComponent {
@Input() module?: CoreCourseModule; // The module of the component.
@Input() courseId?: number; // Course ID the component belongs to.
@Input() module!: CoreCourseModule; // The module of the component.
@Input() courseId!: number; // Course ID the component belongs to.
@Output() dataRetrieved = new EventEmitter<unknown>(); // Called to notify changes the index page from the main component.
loaded = false; // If the component has been loaded.
@ -90,10 +90,10 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
*/
async ngOnInit(): Promise<void> {
this.siteId = CoreSites.getCurrentSiteId();
this.description = this.module?.description;
this.componentId = this.module?.id;
this.externalUrl = this.module?.url;
this.courseId = this.courseId || this.module?.course;
this.description = this.module.description;
this.componentId = this.module.id;
this.externalUrl = this.module.url;
this.courseId = this.courseId || this.module.course!;
this.blog = await AddonBlog.isPluginEnabled();
}
@ -105,8 +105,9 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher> | null, done?: () => void, showErrors: boolean = false): Promise<void> {
async doRefresh(refresher?: IonRefresher | null, done?: () => void, showErrors: boolean = false): Promise<void> {
if (!this.loaded || !this.module) {
// Module can be undefined if course format changes from single activity to weekly/topics.
return;
}
@ -118,7 +119,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
await CoreUtils.ignoreErrors(this.refreshContent(true, showErrors));
refresher?.detail.complete();
refresher?.complete();
done && done();
}
@ -193,12 +194,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
* Fill the context menu options
*/
protected fillContextMenu(refresh: boolean = false): void {
if (!this.module) {
return;
}
// All data obtained, now fill the context menu.
CoreCourseHelper.fillContextMenu(this, this.module, this.courseId!, refresh, this.component);
CoreCourseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component);
}
/**
@ -215,10 +212,10 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
expandDescription(): void {
CoreTextUtils.viewText(Translate.instant('core.description'), this.description!, {
component: this.component,
componentId: this.module?.id,
componentId: this.module.id,
filter: true,
contextLevel: 'module',
instanceId: this.module?.id,
instanceId: this.module.id,
courseId: this.courseId,
});
}
@ -227,7 +224,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
* Go to blog posts.
*/
async gotoBlog(): Promise<void> {
const params: Params = { cmId: this.module?.id };
const params: Params = { cmId: this.module.id };
CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params });
}
@ -238,11 +235,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
* @param done Function to call when done.
*/
prefetch(done?: () => void): void {
if (!this.module) {
return;
}
CoreCourseHelper.contextMenuPrefetch(this, this.module, this.courseId!, done);
CoreCourseHelper.contextMenuPrefetch(this, this.module, this.courseId, done);
}
/**
@ -251,17 +244,13 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
* @param done Function to call when done.
*/
removeFiles(done?: () => void): void {
if (!this.module) {
return;
}
if (this.prefetchStatus == CoreConstants.DOWNLOADING) {
CoreDomUtils.showAlertTranslated(undefined, 'core.course.cannotdeletewhiledownloading');
return;
}
CoreCourseHelper.confirmAndRemoveFiles(this.module, this.courseId!, done);
CoreCourseHelper.confirmAndRemoveFiles(this.module, this.courseId, done);
}
/**
@ -309,13 +298,13 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
* @return Promise resolved when done.
*/
protected async setStatusListener(): Promise<void> {
if (typeof this.statusObserver != 'undefined' || !this.module) {
if (typeof this.statusObserver != 'undefined') {
return;
}
// Listen for changes on this module status.
this.statusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
if (!this.module || data.componentId != this.module.id || data.component != this.component) {
if (data.componentId != this.module.id || data.component != this.component) {
return;
}
@ -327,7 +316,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
}, this.siteId);
// Also, get the current status.
const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(this.module, this.courseId!);
const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(this.module, this.courseId);
this.currentStatus = status;
this.showStatus(status);
@ -350,17 +339,13 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
failed: false,
};
if (!this.module) {
return result;
}
// Get module status to determine if it needs to be downloaded.
await this.setStatusListener();
if (this.currentStatus != CoreConstants.DOWNLOADED) {
// Download content. This function also loads module contents if needed.
try {
await CoreCourseModulePrefetchDelegate.downloadModule(this.module, this.courseId!);
await CoreCourseModulePrefetchDelegate.downloadModule(this.module, this.courseId);
// If we reach here it means the download process already loaded the contents, no need to do it again.
contentsAlreadyLoaded = true;

View File

@ -497,7 +497,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @param afterCompletionChange Whether the refresh is due to a completion change.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, afterCompletionChange?: boolean): Promise<void> {
async doRefresh(refresher?: IonRefresher, done?: () => void, afterCompletionChange?: boolean): Promise<void> {
const promises = this.dynamicComponents?.map(async (component) => {
await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]);
}) || [];
@ -508,7 +508,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
await Promise.all(promises);
refresher?.detail.complete();
refresher?.complete();
done?.();
}

View File

@ -78,7 +78,7 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges {
* @param afterCompletionChange Whether the refresh is due to a completion change.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, afterCompletionChange?: boolean): Promise<void> {
async doRefresh(refresher?: IonRefresher, done?: () => void, afterCompletionChange?: boolean): Promise<void> {
if (afterCompletionChange) {
// Don't refresh the view after a completion change since completion isn't displayed.
return;

View File

@ -16,7 +16,7 @@
</core-context-menu>
</core-navbar-buttons>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!dataLoaded || !displayRefresher" (ionRefresh)="doRefresh($event)">
<ion-refresher slot="fixed" [disabled]="!dataLoaded || !displayRefresher" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
@ -26,4 +26,4 @@
(completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}">
</core-course-format>
</core-loading>
</ion-content>
</ion-content>

View File

@ -327,7 +327,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
* @param refresher Refresher.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>): Promise<void> {
async doRefresh(refresher?: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(this.invalidateData());
try {
@ -339,7 +339,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
await CoreUtils.ignoreErrors(this.formatComponent.doRefresh(refresher));
}
refresher?.detail.complete();
refresher?.complete();
}
}

View File

@ -7,7 +7,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
@ -25,4 +25,4 @@
</ng-container>
</ion-list>
</core-loading>
</ion-content>
</ion-content>

View File

@ -119,13 +119,13 @@ export class CoreCourseListModTypePage implements OnInit {
* @param refresher Refresher.
* @return Promise resolved when done.
*/
async refreshData(refresher: CustomEvent<IonRefresher>): Promise<void> {
async refreshData(refresher: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(CoreCourse.invalidateSections(this.courseId || 0));
try {
await this.fetchData();
} finally {
refresher.detail.complete();
refresher.complete();
}
}

View File

@ -9,7 +9,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="refreshData($event)">
<ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="dataLoaded">

View File

@ -379,7 +379,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
*
* @param refresher The refresher if this was triggered by a Pull To Refresh.
*/
async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> {
async refreshData(refresher?: IonRefresher): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(CoreCourses.invalidateUserCourses());
@ -394,7 +394,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
await Promise.all(promises).finally(() => this.getCourse()).finally(() => {
refresher?.detail.complete();
refresher?.complete();
});
}

View File

@ -184,7 +184,7 @@ export interface CoreCourseModuleMainComponent {
* @param done Function to call when done.
* @return Promise resolved when done.
*/
doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void): Promise<void>;
doRefresh(refresher?: IonRefresher, done?: () => void): Promise<void>;
}
/**

Some files were not shown because too many files have changed in this diff Show More