MOBILE-3926 glossary: Entries swipe navigation

main
Noel De Martin 2021-11-25 13:00:58 +01:00
parent e8d0026995
commit 70ff7a375a
11 changed files with 812 additions and 475 deletions

View File

@ -0,0 +1,381 @@
// (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 { Params } from '@angular/router';
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
import {
AddonModGlossary,
AddonModGlossaryEntry,
AddonModGlossaryGetEntriesOptions,
AddonModGlossaryGetEntriesWSResponse,
AddonModGlossaryGlossary,
AddonModGlossaryProvider,
} from '../services/glossary';
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../services/glossary-offline';
/**
* Provides a collection of glossary entries.
*/
export class AddonModGlossaryEntriesSource extends CoreItemsManagerSource<AddonModGlossaryEntryItem> {
static readonly NEW_ENTRY: AddonModGlossaryNewEntryForm = { newEntry: true };
readonly COURSE_ID: number;
readonly CM_ID: number;
readonly GLOSSARY_PATH_PREFIX: string;
isSearch = false;
hasSearched = false;
fetchMode?: AddonModGlossaryFetchMode;
viewMode?: string;
glossary?: AddonModGlossaryGlossary;
onlineEntries: AddonModGlossaryEntry[] = [];
offlineEntries: AddonModGlossaryOfflineEntry[] = [];
protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse;
protected fetchInvalidate?: () => Promise<void>;
constructor(courseId: number, cmId: number, glossaryPathPrefix: string) {
super();
this.COURSE_ID = courseId;
this.CM_ID = cmId;
this.GLOSSARY_PATH_PREFIX = glossaryPathPrefix;
}
/**
* Type guard to infer NewEntryForm objects.
*
* @param entry Item to check.
* @return Whether the item is a new entry form.
*/
isNewEntryForm(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryNewEntryForm {
return 'newEntry' in entry;
}
/**
* Type guard to infer entry objects.
*
* @param entry Item to check.
* @return Whether the item is an offline entry.
*/
isOnlineEntry(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryEntry {
return 'id' in entry;
}
/**
* Type guard to infer entry objects.
*
* @param entry Item to check.
* @return Whether the item is an offline entry.
*/
isOfflineEntry(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryOfflineEntry {
return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry);
}
/**
* @inheritdoc
*/
getItemPath(entry: AddonModGlossaryEntryItem): string {
if (this.isOnlineEntry(entry)) {
return `${this.GLOSSARY_PATH_PREFIX}entry/${entry.id}`;
}
if (this.isOfflineEntry(entry)) {
return `${this.GLOSSARY_PATH_PREFIX}edit/${entry.timecreated}`;
}
return `${this.GLOSSARY_PATH_PREFIX}edit/0`;
}
/**
* @inheritdoc
*/
getItemQueryParams(entry: AddonModGlossaryEntryItem): Params {
const params: Params = {
cmId: this.CM_ID,
courseId: this.COURSE_ID,
};
if (this.isOfflineEntry(entry)) {
params.concept = entry.concept;
}
return params;
}
/**
* @inheritdoc
*/
getPagesLoaded(): number {
if (this.items === null) {
return 0;
}
return Math.ceil(this.onlineEntries.length / this.getPageLength());
}
/**
* Start searching.
*/
startSearch(): void {
this.isSearch = true;
}
/**
* Stop searching and restore unfiltered collection.
*
* @param cachedOnlineEntries Cached online entries.
* @param hasMoreOnlineEntries Whether there were more online entries.
*/
stopSearch(cachedOnlineEntries: AddonModGlossaryEntry[], hasMoreOnlineEntries: boolean): void {
if (!this.fetchMode) {
return;
}
this.isSearch = false;
this.hasSearched = false;
this.onlineEntries = cachedOnlineEntries;
this.hasMoreItems = hasMoreOnlineEntries;
}
/**
* Set search query.
*
* @param query Search query.
*/
search(query: string): void {
if (!this.glossary) {
return;
}
this.fetchFunction = AddonModGlossary.getEntriesBySearch.bind(
AddonModGlossary.instance,
this.glossary.id,
query,
true,
'CONCEPT',
'ASC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesBySearch.bind(
AddonModGlossary.instance,
this.glossary.id,
query,
true,
'CONCEPT',
'ASC',
);
this.hasSearched = true;
}
/**
* Load glossary.
*/
async loadGlossary(): Promise<void> {
this.glossary = await AddonModGlossary.getGlossary(this.COURSE_ID, this.CM_ID);
}
/**
* Invalidate glossary cache.
*/
async invalidateCache(): Promise<void> {
await Promise.all([
AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID),
this.fetchInvalidate && this.fetchInvalidate(),
this.glossary && AddonModGlossary.invalidateCategories(this.glossary.id),
]);
}
/**
* Change fetch mode.
*
* @param mode New mode.
*/
switchMode(mode: AddonModGlossaryFetchMode): void {
if (!this.glossary) {
throw new Error('Can\'t switch entries mode without a glossary!');
}
this.fetchMode = mode;
this.isSearch = false;
switch (mode) {
case 'author_all':
// Browse by author.
this.viewMode = 'author';
this.fetchFunction = AddonModGlossary.getEntriesByAuthor.bind(
AddonModGlossary.instance,
this.glossary.id,
'ALL',
'LASTNAME',
'ASC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByAuthor.bind(
AddonModGlossary.instance,
this.glossary.id,
'ALL',
'LASTNAME',
'ASC',
);
break;
case 'cat_all':
// Browse by category.
this.viewMode = 'cat';
this.fetchFunction = AddonModGlossary.getEntriesByCategory.bind(
AddonModGlossary.instance,
this.glossary.id,
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByCategory.bind(
AddonModGlossary.instance,
this.glossary.id,
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
);
break;
case 'newest_first':
// Newest first.
this.viewMode = 'date';
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary.id,
'CREATION',
'DESC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary.id,
'CREATION',
'DESC',
);
break;
case 'recently_updated':
// Recently updated.
this.viewMode = 'date';
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary.id,
'UPDATE',
'DESC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary.id,
'UPDATE',
'DESC',
);
break;
case 'letter_all':
default:
// Consider it is 'letter_all'.
this.viewMode = 'letter';
this.fetchMode = 'letter_all';
this.fetchFunction = AddonModGlossary.getEntriesByLetter.bind(
AddonModGlossary.instance,
this.glossary.id,
'ALL',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByLetter.bind(
AddonModGlossary.instance,
this.glossary.id,
'ALL',
);
break;
}
}
/**
* @inheritdoc
*/
protected async loadPageItems(page: number): Promise<{ items: AddonModGlossaryEntryItem[]; hasMoreItems: boolean }> {
const glossary = this.glossary;
const fetchFunction = this.fetchFunction;
if (!glossary || !fetchFunction) {
throw new Error('Can\'t load entries without glossary or fetch function');
}
const entries: AddonModGlossaryEntryItem[] = [];
if (page === 0) {
const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(glossary.id);
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
entries.push(AddonModGlossaryEntriesSource.NEW_ENTRY);
entries.push(...offlineEntries);
}
const from = page * this.getPageLength();
const pageEntries = await fetchFunction({ from, cmId: this.CM_ID });
entries.push(...pageEntries.entries);
return {
items: entries,
hasMoreItems: from + pageEntries.entries.length < pageEntries.count,
};
}
/**
* @inheritdoc
*/
protected getPageLength(): number {
return AddonModGlossaryProvider.LIMIT_ENTRIES;
}
/**
* @inheritdoc
*/
protected setItems(entries: AddonModGlossaryEntryItem[], hasMoreItems: boolean): void {
this.onlineEntries = [];
this.offlineEntries = [];
entries.forEach(entry => {
this.isOnlineEntry(entry) && this.onlineEntries.push(entry);
this.isOfflineEntry(entry) && this.offlineEntries.push(entry);
});
super.setItems(entries, hasMoreItems);
}
/**
* @inheritdoc
*/
reset(): void {
this.onlineEntries = [];
this.offlineEntries = [];
super.reset();
}
}
/**
* Type of items that can be held by the entries manager.
*/
export type AddonModGlossaryEntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | AddonModGlossaryNewEntryForm;
/**
* Type to select the new entry form.
*/
export type AddonModGlossaryNewEntryForm = { newEntry: true };
/**
* Fetch mode to sort entries.
*/
export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';

View File

@ -0,0 +1,52 @@
// (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 { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from './glossary-entries-source';
/**
* Helper to manage swiping within a collection of glossary entries.
*/
export abstract class AddonModGlossaryEntriesSwipeManager
extends CoreSwipeItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
/**
* @inheritdoc
*/
async navigateToNextItem(): Promise<void> {
let delta = -1;
const item = await this.getItemBy(-1);
if (item && this.getSource().isNewEntryForm(item)) {
delta--;
}
await this.navigateToItemBy(delta, 'back');
}
/**
* @inheritdoc
*/
async navigateToPreviousItem(): Promise<void> {
let delta = 1;
const item = await this.getItemBy(1);
if (item && this.getSource().isNewEntryForm(item)) {
delta++;
}
await this.navigateToItemBy(delta, 'forward');
}
}

View File

@ -54,7 +54,7 @@
[component]="component" [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline || hasOfflineRatings"> [component]="component" [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline || hasOfflineRatings">
</core-course-module-info> </core-course-module-info>
<ion-list *ngIf="!isSearch && entries.offlineEntries.length > 0"> <ion-list *ngIf="!isSearch && entries && entries.offlineEntries.length > 0">
<ion-item-divider> <ion-item-divider>
<ion-label> <ion-label>
<h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2> <h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
@ -70,7 +70,7 @@
</ion-item> </ion-item>
</ion-list> </ion-list>
<ion-list *ngIf="entries.onlineEntries.length > 0"> <ion-list *ngIf="entries && entries.onlineEntries.length > 0">
<ng-container *ngFor="let entry of entries.onlineEntries; let index = index"> <ng-container *ngFor="let entry of entries.onlineEntries; let index = index">
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])"> <ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
<ion-label> <ion-label>
@ -88,11 +88,11 @@
</ng-container> </ng-container>
</ion-list> </ion-list>
<core-empty-box *ngIf="entries.empty && (!isSearch || hasSearched)" icon="fas-list" <core-empty-box *ngIf="(!entries || entries.empty) && (!isSearch || hasSearched)" icon="fas-list"
[message]="'addon.mod_glossary.noentriesfound' | translate"> [message]="'addon.mod_glossary.noentriesfound' | translate">
</core-empty-box> </core-empty-box>
<core-infinite-loading [enabled]="!entries.completed" [error]="loadMoreError" (action)="loadMoreEntries($event)"> <core-infinite-loading [enabled]="entries && !entries.completed" [error]="loadMoreError" (action)="loadMoreEntries($event)">
</core-infinite-loading> </core-infinite-loading>
</core-loading> </core-loading>

View File

@ -14,8 +14,9 @@
import { ContextLevel } from '@/core/constants'; import { ContextLevel } from '@/core/constants';
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core'; import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { CorePageItemsListManager } from '@classes/page-items-list-manager'; import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
@ -29,16 +30,19 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
AddonModGlossaryEntriesSource,
AddonModGlossaryEntryItem,
AddonModGlossaryFetchMode,
} from '../../classes/glossary-entries-source';
import { import {
AddonModGlossary, AddonModGlossary,
AddonModGlossaryEntry, AddonModGlossaryEntry,
AddonModGlossaryEntryWithCategory, AddonModGlossaryEntryWithCategory,
AddonModGlossaryGetEntriesOptions,
AddonModGlossaryGetEntriesWSResponse,
AddonModGlossaryGlossary, AddonModGlossaryGlossary,
AddonModGlossaryProvider, AddonModGlossaryProvider,
} from '../../services/glossary'; } from '../../services/glossary';
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../../services/glossary-offline'; import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
import { import {
AddonModGlossaryAutoSyncData, AddonModGlossaryAutoSyncData,
AddonModGlossarySyncProvider, AddonModGlossarySyncProvider,
@ -63,23 +67,17 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
component = AddonModGlossaryProvider.COMPONENT; component = AddonModGlossaryProvider.COMPONENT;
moduleName = 'glossary'; moduleName = 'glossary';
isSearch = false;
hasSearched = false;
canAdd = false; canAdd = false;
loadMoreError = false; loadMoreError = false;
loadingMessage?: string; loadingMessage: string;
entries: AddonModGlossaryEntriesManager; entries!: AddonModGlossaryEntriesManager;
hasOfflineRatings = false; hasOfflineRatings = false;
glossary?: AddonModGlossaryGlossary;
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED; protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse;
protected fetchInvalidate?: () => Promise<void>;
protected addEntryObserver?: CoreEventObserver; protected addEntryObserver?: CoreEventObserver;
protected fetchMode?: AddonModGlossaryFetchMode;
protected viewMode?: string;
protected fetchedEntriesCanLoadMore = false; protected fetchedEntriesCanLoadMore = false;
protected fetchedEntries: AddonModGlossaryEntry[] = []; protected fetchedEntries: AddonModGlossaryEntry[] = [];
protected sourceUnsubscribe?: () => void;
protected ratingOfflineObserver?: CoreEventObserver; protected ratingOfflineObserver?: CoreEventObserver;
protected ratingSyncObserver?: CoreEventObserver; protected ratingSyncObserver?: CoreEventObserver;
@ -87,26 +85,47 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false; showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
constructor( constructor(
route: ActivatedRoute, protected route: ActivatedRoute,
protected content?: IonContent, protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage, @Optional() protected courseContentsPage?: CoreCourseContentsPage,
) { ) {
super('AddonModGlossaryIndexComponent', content, courseContentsPage); super('AddonModGlossaryIndexComponent', content, courseContentsPage);
this.entries = new AddonModGlossaryEntriesManager( this.loadingMessage = Translate.instant('core.loading');
route.component, }
this,
courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : '', get glossary(): AddonModGlossaryGlossary | undefined {
); return this.entries.getSource().glossary;
}
get isSearch(): boolean {
return this.entries.getSource().isSearch;
}
get hasSearched(): boolean {
return this.entries.getSource().hasSearched;
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
super.ngOnInit(); await super.ngOnInit();
this.loadingMessage = Translate.instant('core.loading'); // Initialize entries manager.
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
AddonModGlossaryEntriesSource,
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
);
this.entries = new AddonModGlossaryEntriesManager(
source,
this.route.component,
);
this.sourceUnsubscribe = source.addListener({
onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)),
});
// When an entry is added, we reload the data. // When an entry is added, we reload the data.
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => { this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
@ -143,11 +162,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
return; return;
} }
this.entries.start(this.splitView); await this.entries.start(this.splitView);
try { try {
await AddonModGlossary.logView(this.glossary.id, this.viewMode!, this.glossary.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch (error) { } catch (error) {
// Ignore errors. // Ignore errors.
@ -159,14 +176,18 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
*/ */
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
try { try {
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.module.id); await this.entries.getSource().loadGlossary();
if (!this.glossary) {
return;
}
this.description = this.glossary.intro || this.description; this.description = this.glossary.intro || this.description;
this.canAdd = !!this.glossary.canaddentry || false; this.canAdd = !!this.glossary.canaddentry || false;
this.dataRetrieved.emit(this.glossary); this.dataRetrieved.emit(this.glossary);
if (!this.fetchMode) { if (!this.entries.getSource().fetchMode) {
this.switchMode('letter_all'); this.switchMode('letter_all');
} }
@ -177,7 +198,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
const [hasOfflineRatings] = await Promise.all([ const [hasOfflineRatings] = await Promise.all([
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule), CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
this.fetchEntries(), refresh ? this.entries.reload() : this.entries.loadNextPage(),
]); ]);
this.hasOfflineRatings = hasOfflineRatings; this.hasOfflineRatings = hasOfflineRatings;
@ -186,59 +207,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
} }
} }
/**
* Convenience function to fetch entries.
*
* @param append True if fetched entries are appended to exsiting ones.
* @return Promise resolved when done.
*/
protected async fetchEntries(append: boolean = false): Promise<void> {
if (!this.fetchFunction) {
return;
}
this.loadMoreError = false;
const from = append ? this.entries.onlineEntries.length : 0;
const result = await this.fetchFunction({
from: from,
cmId: this.module.id,
});
const hasMoreEntries = from + result.entries.length < result.count;
if (append) {
this.entries.setItems(this.entries.items.concat(result.entries), hasMoreEntries);
} else {
this.entries.setOnlineEntries(result.entries, hasMoreEntries);
}
// Now get the ofline entries.
// Check if there are responses stored in offline.
const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(this.glossary!.id);
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
this.hasOffline = !!offlineEntries.length;
this.entries.setOfflineEntries(offlineEntries);
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected async invalidateContent(): Promise<void> { protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = []; await this.entries.getSource().invalidateCache();
if (this.fetchInvalidate) {
promises.push(this.fetchInvalidate());
}
promises.push(AddonModGlossary.invalidateCourseGlossaries(this.courseId));
if (this.glossary) {
promises.push(AddonModGlossary.invalidateCategories(this.glossary.id));
}
await Promise.all(promises);
} }
/** /**
@ -277,109 +250,50 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
* @param mode New mode. * @param mode New mode.
*/ */
protected switchMode(mode: AddonModGlossaryFetchMode): void { protected switchMode(mode: AddonModGlossaryFetchMode): void {
this.fetchMode = mode; this.entries.getSource().switchMode(mode);
this.isSearch = false;
switch (mode) { switch (mode) {
case 'author_all': case 'author_all':
// Browse by author. // Browse by author.
this.viewMode = 'author';
this.fetchFunction = AddonModGlossary.getEntriesByAuthor.bind(
AddonModGlossary.instance,
this.glossary!.id,
'ALL',
'LASTNAME',
'ASC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByAuthor.bind(
AddonModGlossary.instance,
this.glossary!.id,
'ALL',
'LASTNAME',
'ASC',
);
this.getDivider = (entry) => entry.userfullname; this.getDivider = (entry) => entry.userfullname;
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid; this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
break; break;
case 'cat_all': case 'cat_all': {
// Browse by category. // Browse by category.
this.viewMode = 'cat'; const getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
this.fetchFunction = AddonModGlossary.getEntriesByCategory.bind(
AddonModGlossary.instance, this.getDivider = getDivider;
this.glossary!.id, this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous);
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByCategory.bind(
AddonModGlossary.instance,
this.glossary!.id,
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
);
this.getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
break; break;
}
case 'newest_first': case 'newest_first':
// Newest first. // Newest first.
this.viewMode = 'date';
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary!.id,
'CREATION',
'DESC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary!.id,
'CREATION',
'DESC',
);
this.getDivider = undefined; this.getDivider = undefined;
this.showDivider = () => false; this.showDivider = () => false;
break; break;
case 'recently_updated': case 'recently_updated':
// Recently updated. // Recently updated.
this.viewMode = 'date';
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary!.id,
'UPDATE',
'DESC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary!.id,
'UPDATE',
'DESC',
);
this.getDivider = undefined; this.getDivider = undefined;
this.showDivider = () => false; this.showDivider = () => false;
break; break;
case 'letter_all': case 'letter_all':
default: default: {
// Consider it is 'letter_all'. // Consider it is 'letter_all'.
this.viewMode = 'letter'; const getDivider = (entry) => {
this.fetchMode = 'letter_all';
this.fetchFunction = AddonModGlossary.getEntriesByLetter.bind(
AddonModGlossary.instance,
this.glossary!.id,
'ALL',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByLetter.bind(
AddonModGlossary.instance,
this.glossary!.id,
'ALL',
);
this.getDivider = (entry) => {
// Try to get the first letter without HTML tags. // Try to get the first letter without HTML tags.
const noTags = CoreTextUtils.cleanTags(entry.concept); const noTags = CoreTextUtils.cleanTags(entry.concept);
return (noTags || entry.concept).substr(0, 1).toUpperCase(); return (noTags || entry.concept).substr(0, 1).toUpperCase();
}; };
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
this.getDivider = getDivider;
this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous);
break; break;
}
} }
} }
@ -391,7 +305,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
*/ */
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> { async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
try { try {
await this.fetchEntries(true); this.loadMoreError = false;
await this.entries.loadNextPage();
} catch (error) { } catch (error) {
this.loadMoreError = true; this.loadMoreError = true;
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true); CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
@ -406,21 +322,34 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
* @param event Event. * @param event Event.
*/ */
async openModePicker(event: MouseEvent): Promise<void> { async openModePicker(event: MouseEvent): Promise<void> {
const mode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({ if (!this.glossary) {
return;
}
const previousMode = this.entries.getSource().fetchMode;
const newMode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
component: AddonModGlossaryModePickerPopoverComponent, component: AddonModGlossaryModePickerPopoverComponent,
componentProps: { componentProps: {
browseModes: this.glossary!.browsemodes, browseModes: this.glossary.browsemodes,
selectedMode: this.isSearch ? '' : this.fetchMode, selectedMode: this.isSearch ? '' : previousMode,
}, },
event, event,
}); });
if (mode) { if (!newMode) {
if (mode !== this.fetchMode) { return;
this.changeFetchMode(mode); }
} else if (this.isSearch) {
this.toggleSearch(); if (newMode !== previousMode) {
} this.changeFetchMode(newMode);
return;
}
if (this.isSearch) {
this.toggleSearch();
return;
} }
} }
@ -429,20 +358,22 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
*/ */
toggleSearch(): void { toggleSearch(): void {
if (this.isSearch) { if (this.isSearch) {
this.isSearch = false; const fetchMode = this.entries.getSource().fetchMode;
this.hasSearched = false;
this.entries.setOnlineEntries(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
this.switchMode(this.fetchMode!);
} else {
// Search for entries. The fetch function will be set when searching.
this.getDivider = undefined;
this.showDivider = () => false;
this.isSearch = true;
this.fetchedEntries = this.entries.onlineEntries; fetchMode && this.switchMode(fetchMode);
this.fetchedEntriesCanLoadMore = !this.entries.completed; this.entries.getSource().stopSearch(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
this.entries.setItems([], false);
return;
} }
// Search for entries. The fetch function will be set when searching.
this.fetchedEntries = this.entries.getSource().onlineEntries;
this.fetchedEntriesCanLoadMore = !this.entries.completed;
this.getDivider = undefined;
this.showDivider = () => false;
this.entries.reset();
this.entries.getSource().startSearch();
} }
/** /**
@ -451,7 +382,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
* @param mode Mode. * @param mode Mode.
*/ */
changeFetchMode(mode: AddonModGlossaryFetchMode): void { changeFetchMode(mode: AddonModGlossaryFetchMode): void {
this.isSearch = false;
this.loadingMessage = Translate.instant('core.loading'); this.loadingMessage = Translate.instant('core.loading');
this.content?.scrollToTop(); this.content?.scrollToTop();
this.switchMode(mode); this.switchMode(mode);
@ -463,7 +393,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
* Opens new entry editor. * Opens new entry editor.
*/ */
openNewEntry(): void { openNewEntry(): void {
this.entries.select({ newEntry: true }); this.entries.select(AddonModGlossaryEntriesSource.NEW_ENTRY);
} }
/** /**
@ -473,24 +403,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
*/ */
search(query: string): void { search(query: string): void {
this.loadingMessage = Translate.instant('core.searching'); this.loadingMessage = Translate.instant('core.searching');
this.fetchFunction = AddonModGlossary.getEntriesBySearch.bind(
AddonModGlossary.instance,
this.glossary!.id,
query,
true,
'CONCEPT',
'ASC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesBySearch.bind(
AddonModGlossary.instance,
this.glossary!.id,
query,
true,
'CONCEPT',
'ASC',
);
this.loaded = false; this.loaded = false;
this.hasSearched = true;
this.entries.getSource().search(query);
this.loadContent(); this.loadContent();
} }
@ -503,154 +418,44 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
this.addEntryObserver?.off(); this.addEntryObserver?.off();
this.ratingOfflineObserver?.off(); this.ratingOfflineObserver?.off();
this.ratingSyncObserver?.off(); this.ratingSyncObserver?.off();
this.sourceUnsubscribe?.call(null);
this.entries.destroy();
} }
} }
/**
* Type to select the new entry form.
*/
type NewEntryForm = { newEntry: true };
/**
* Type of items that can be held by the entries manager.
*/
type EntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | NewEntryForm;
/** /**
* Entries manager. * Entries manager.
*/ */
class AddonModGlossaryEntriesManager extends CorePageItemsListManager<EntryItem> { class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
onlineEntries: AddonModGlossaryEntry[] = []; get offlineEntries(): AddonModGlossaryOfflineEntry[] {
offlineEntries: AddonModGlossaryOfflineEntry[] = []; return this.getSource().offlineEntries;
protected glossaryPathPrefix: string;
protected component: AddonModGlossaryIndexComponent;
constructor(
pageComponent: unknown,
component: AddonModGlossaryIndexComponent,
glossaryPathPrefix: string,
) {
super(pageComponent);
this.component = component;
this.glossaryPathPrefix = glossaryPathPrefix;
} }
/** get onlineEntries(): AddonModGlossaryEntry[] {
* Type guard to infer NewEntryForm objects. return this.getSource().onlineEntries;
*
* @param entry Item to check.
* @return Whether the item is a new entry form.
*/
isNewEntryForm(entry: EntryItem): entry is NewEntryForm {
return 'newEntry' in entry;
}
/**
* Type guard to infer entry objects.
*
* @param entry Item to check.
* @return Whether the item is an offline entry.
*/
isOfflineEntry(entry: EntryItem): entry is AddonModGlossaryOfflineEntry {
return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry);
}
/**
* Type guard to infer entry objects.
*
* @param entry Item to check.
* @return Whether the item is an offline entry.
*/
isOnlineEntry(entry: EntryItem): entry is AddonModGlossaryEntry {
return 'id' in entry;
}
/**
* Update online entries items.
*
* @param onlineEntries Online entries.
*/
setOnlineEntries(onlineEntries: AddonModGlossaryEntry[], hasMoreItems: boolean = false): void {
this.setItems((<EntryItem[]> this.offlineEntries).concat(onlineEntries), hasMoreItems);
}
/**
* Update offline entries items.
*
* @param offlineEntries Offline entries.
*/
setOfflineEntries(offlineEntries: AddonModGlossaryOfflineEntry[]): void {
this.setItems((<EntryItem[]> offlineEntries).concat(this.onlineEntries), this.hasMoreItems);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
setItems(entries: EntryItem[], hasMoreItems: boolean = false): void { protected getDefaultItem(): AddonModGlossaryEntryItem | null {
super.setItems(entries, hasMoreItems); return this.getSource().onlineEntries[0] || null;
this.onlineEntries = [];
this.offlineEntries = [];
this.items.forEach(entry => {
if (this.isOfflineEntry(entry)) {
this.offlineEntries.push(entry);
} else if (this.isOnlineEntry(entry)) {
this.onlineEntries.push(entry);
}
});
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
resetItems(): void { protected async logActivity(): Promise<void> {
super.resetItems(); const glossary = this.getSource().glossary;
this.onlineEntries = []; const viewMode = this.getSource().viewMode;
this.offlineEntries = [];
}
/** if (!glossary || !viewMode) {
* @inheritdoc return;
*/
protected getItemPath(entry: EntryItem): string {
if (this.isOnlineEntry(entry)) {
return `${this.glossaryPathPrefix}entry/${entry.id}`;
} }
if (this.isOfflineEntry(entry)) { await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
return `${this.glossaryPathPrefix}edit/${entry.timecreated}`;
}
return `${this.glossaryPathPrefix}edit/0`;
}
/**
* @inheritdoc
*/
getItemQueryParams(entry: EntryItem): Params {
const params: Params = {
cmId: this.component.module.id,
courseId: this.component.courseId,
};
if (this.isOfflineEntry(entry)) {
params.concept = entry.concept;
}
return params;
}
/**
* @inheritdoc
*/
protected getDefaultItem(): EntryItem | null {
return this.onlineEntries[0] || null;
} }
} }
export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';

View File

@ -14,7 +14,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { PopoverController } from '@singletons'; import { PopoverController } from '@singletons';
import { AddonModGlossaryFetchMode } from '../index'; import { AddonModGlossaryFetchMode } from '../../classes/glossary-entries-source';
/** /**
* Component to display the mode picker. * Component to display the mode picker.

View File

@ -51,10 +51,12 @@ const mainMenuRoutes: Routes = [
{ {
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
data: { swipeEnabled: false },
}, },
{ {
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
data: { swipeEnabled: false },
}, },
{ {
path: AddonModGlossaryModuleHandlerService.PAGE_NAME, path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
@ -65,10 +67,12 @@ const mainMenuRoutes: Routes = [
{ {
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
}, },
{ {
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
}, },
], ],
() => CoreScreen.isMobile, () => CoreScreen.isMobile,
@ -80,10 +84,12 @@ const courseContentsRoutes: Routes = conditionalRoutes(
{ {
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
}, },
{ {
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
}, },
], ],
() => CoreScreen.isTablet, () => CoreScreen.isTablet,

View File

@ -12,72 +12,75 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<core-loading [hideUntil]="loaded"> <core-swipe-navigation [manager]="entries">
<form #editFormEl *ngIf="glossary"> <core-loading [hideUntil]="loaded">
<ion-item> <form #editFormEl *ngIf="glossary">
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label> <ion-item>
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept" name="concept"> <ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
</ion-input> <ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept"
</ion-item> name="concept">
<ion-item> </ion-input>
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label> </ion-item>
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)" <ion-item>
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component" <ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId" elementId="definition_editor" <core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
[draftExtraParams]="editorExtraParams"> [placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
</core-rich-text-editor> [componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId"
</ion-item> elementId="definition_editor" [draftExtraParams]="editorExtraParams">
<ion-item *ngIf="categories.length > 0"> </core-rich-text-editor>
<ion-label position="stacked" id="addon-mod-glossary-categories-label"> </ion-item>
{{ 'addon.mod_glossary.categories' | translate }} <ion-item *ngIf="categories.length > 0">
</ion-label> <ion-label position="stacked" id="addon-mod-glossary-categories-label">
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label" {{ 'addon.mod_glossary.categories' | translate }}
interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories" </ion-label>
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}"> <ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label"
<ion-select-option *ngFor="let category of categories" [value]="category.id"> interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
{{ category.name }} [interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
</ion-select-option> <ion-select-option *ngFor="let category of categories" [value]="category.id">
</ion-select> {{ category.name }}
</ion-item> </ion-select-option>
<ion-item> </ion-select>
<ion-label position="stacked" id="addon-mod-glossary-aliases-label"> </ion-item>
{{ 'addon.mod_glossary.aliases' | translate }} <ion-item>
</ion-label> <ion-label position="stacked" id="addon-mod-glossary-aliases-label">
<ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases" {{ 'addon.mod_glossary.aliases' | translate }}
aria-labelledby="addon-mod-glossary-aliases-label" name="aliases"> </ion-label>
</ion-textarea> <ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases"
</ion-item> aria-labelledby="addon-mod-glossary-aliases-label" name="aliases">
<ion-item-divider> </ion-textarea>
<ion-label> </ion-item>
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
[courseId]="courseId">
</core-attachments>
<ng-container *ngIf="glossary.usedynalink">
<ion-item-divider> <ion-item-divider>
<ion-label> <ion-label>
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2> <h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
</ion-label> </ion-label>
</ion-item-divider> </ion-item-divider>
<ion-item class="ion-text-wrap"> <core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label> [courseId]="courseId">
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle> </core-attachments>
</ion-item> <ng-container *ngIf="glossary.usedynalink">
<ion-item class="ion-text-wrap"> <ion-item-divider>
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label> <ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive"> <h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
</ion-toggle> </ion-label>
</ion-item> </ion-item-divider>
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label> <ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle> <ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
</ion-item> </ion-item>
</ng-container> <ion-item class="ion-text-wrap">
<ion-button class="ion-margin" expand="block" [disabled]="!entry.concept || !entry.definition" (click)="save()"> <ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
{{ 'core.save' | translate }} <ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
</ion-button> </ion-toggle>
</form> </ion-item>
</core-loading> <ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle>
</ion-item>
</ng-container>
<ion-button class="ion-margin" expand="block" [disabled]="!entry.concept || !entry.definition" (click)="save()">
{{ 'core.save' | translate }}
</ion-button>
</form>
</core-loading>
</core-swipe-navigation>
</ion-content> </ion-content>

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, ViewChild, ElementRef, Optional } from '@angular/core'; import { Component, OnInit, ViewChild, ElementRef, Optional, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CanLeave } from '@guards/can-leave'; import { CanLeave } from '@guards/can-leave';
@ -26,6 +28,8 @@ import { CoreTextUtils } from '@services/utils/text';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
import { import {
AddonModGlossary, AddonModGlossary,
AddonModGlossaryCategory, AddonModGlossaryCategory,
@ -45,7 +49,7 @@ import { AddonModGlossaryOffline } from '../../services/glossary-offline';
selector: 'page-addon-mod-glossary-edit', selector: 'page-addon-mod-glossary-edit',
templateUrl: 'edit.html', templateUrl: 'edit.html',
}) })
export class AddonModGlossaryEditPage implements OnInit, CanLeave { export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
@ViewChild('editFormEl') formElement?: ElementRef; @ViewChild('editFormEl') formElement?: ElementRef;
@ -64,6 +68,8 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
timecreated: 0, timecreated: 0,
}; };
entries?: AddonModGlossaryEditEntriesSwipeManager;
options = { options = {
categories: <string[]> [], categories: <string[]> [],
aliases: '', aliases: '',
@ -80,18 +86,30 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
protected originalData?: AddonModGlossaryNewEntryWithFiles; protected originalData?: AddonModGlossaryNewEntryWithFiles;
protected saved = false; protected saved = false;
constructor(@Optional() protected splitView: CoreSplitViewComponent) {} constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
/** /**
* Component being initialized. * Component being initialized.
*/ */
ngOnInit(): void { async ngOnInit(): Promise<void> {
try { try {
const routeData = this.route.snapshot.data;
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated'); this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated');
this.concept = CoreNavigator.getRouteParam<string>('concept') || ''; this.concept = CoreNavigator.getRouteParam<string>('concept') || '';
this.editorExtraParams.timecreated = this.timecreated; this.editorExtraParams.timecreated = this.timecreated;
if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) {
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
AddonModGlossaryEntriesSource,
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
);
this.entries = new AddonModGlossaryEditEntriesSwipeManager(source);
await this.entries.start();
}
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModal(error); CoreDomUtils.showErrorModal(error);
@ -103,6 +121,13 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
this.fetchData(); this.fetchData();
} }
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.entries?.destroy();
}
/** /**
* Fetch required data. * Fetch required data.
* *
@ -134,7 +159,11 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async loadOfflineData(): Promise<void> { protected async loadOfflineData(): Promise<void> {
const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary!.id, this.concept, this.timecreated); if (!this.glossary) {
return;
}
const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary.id, this.concept, this.timecreated);
this.entry.concept = entry.concept || ''; this.entry.concept = entry.concept || '';
this.entry.definition = entry.definition || ''; this.entry.definition = entry.definition || '';
@ -159,7 +188,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
// Treat offline attachments if any. // Treat offline attachments if any.
if (entry.attachments?.offline) { if (entry.attachments?.offline) {
this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary!.id, entry.concept, entry.timecreated); this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary.id, entry.concept, entry.timecreated);
this.originalData.files = this.attachments.slice(); this.originalData.files = this.attachments.slice();
} }
@ -236,6 +265,10 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
definition = CoreTextUtils.formatHtmlLines(definition); definition = CoreTextUtils.formatHtmlLines(definition);
try { try {
if (!this.glossary) {
return;
}
// Upload attachments first if any. // Upload attachments first if any.
const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated); const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
@ -244,7 +277,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
categories: this.options.categories.join(','), categories: this.options.categories.join(','),
}; };
if (this.glossary!.usedynalink) { if (this.glossary.usedynalink) {
options.usedynalink = this.options.usedynalink ? 1 : 0; options.usedynalink = this.options.usedynalink ? 1 : 0;
if (this.options.usedynalink) { if (this.options.usedynalink) {
options.casesensitive = this.options.casesensitive ? 1 : 0; options.casesensitive = this.options.casesensitive ? 1 : 0;
@ -253,9 +286,9 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
} }
if (saveOffline) { if (saveOffline) {
if (this.entry && !this.glossary!.allowduplicatedentries) { if (this.entry && !this.glossary.allowduplicatedentries) {
// Check if the entry is duplicated in online or offline mode. // Check if the entry is duplicated in online or offline mode.
const isUsed = await AddonModGlossary.isConceptUsed(this.glossary!.id, this.entry.concept, { const isUsed = await AddonModGlossary.isConceptUsed(this.glossary.id, this.entry.concept, {
timeCreated: this.entry.timecreated, timeCreated: this.entry.timecreated,
cmId: this.cmId, cmId: this.cmId,
}); });
@ -268,7 +301,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
// Save entry in offline. // Save entry in offline.
await AddonModGlossaryOffline.addNewEntry( await AddonModGlossaryOffline.addNewEntry(
this.glossary!.id, this.glossary.id,
this.entry.concept, this.entry.concept,
definition, definition,
this.courseId, this.courseId,
@ -283,7 +316,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
// Try to send it to server. // Try to send it to server.
// Don't allow offline if there are attachments since they were uploaded fine. // Don't allow offline if there are attachments since they were uploaded fine.
await AddonModGlossary.addEntry( await AddonModGlossary.addEntry(
this.glossary!.id, this.glossary.id,
this.entry.concept, this.entry.concept,
definition, definition,
this.courseId, this.courseId,
@ -293,7 +326,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
timeCreated: timecreated, timeCreated: timecreated,
discardEntry: this.entry, discardEntry: this.entry,
allowOffline: !this.attachments.length, allowOffline: !this.attachments.length,
checkDuplicates: !this.glossary!.allowduplicatedentries, checkDuplicates: !this.glossary.allowduplicatedentries,
}, },
); );
} }
@ -303,12 +336,12 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
if (entryId) { if (entryId) {
// Data sent to server, delete stored files (if any). // Data sent to server, delete stored files (if any).
AddonModGlossaryHelper.deleteStoredFiles(this.glossary!.id, this.entry.concept, timecreated); AddonModGlossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated);
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
} }
CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, { CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, {
glossaryId: this.glossary!.id, glossaryId: this.glossary.id,
entryId: entryId, entryId: entryId,
}, CoreSites.getCurrentSiteId()); }, CoreSites.getCurrentSiteId());
@ -342,7 +375,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
protected async uploadAttachments( protected async uploadAttachments(
timecreated: number, timecreated: number,
): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> { ): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
if (!this.attachments.length) { if (!this.attachments.length || !this.glossary) {
return { return {
saveOffline: false, saveOffline: false,
}; };
@ -352,7 +385,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles( const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
this.attachments, this.attachments,
AddonModGlossaryProvider.COMPONENT, AddonModGlossaryProvider.COMPONENT,
this.glossary!.id, this.glossary.id,
); );
return { return {
@ -362,7 +395,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
} catch { } catch {
// Cannot upload them in online, save them in offline. // Cannot upload them in online, save them in offline.
const attachmentsResult = await AddonModGlossaryHelper.storeFiles( const attachmentsResult = await AddonModGlossaryHelper.storeFiles(
this.glossary!.id, this.glossary.id,
this.entry.concept, this.entry.concept,
timecreated, timecreated,
this.attachments, this.attachments,
@ -387,3 +420,17 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
} }
} }
/**
* Helper to manage swiping within a collection of glossary entries.
*/
class AddonModGlossaryEditEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
/**
* @inheritdoc
*/
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
return `${this.getSource().GLOSSARY_PATH_PREFIX}edit/${route.params.timecreated}`;
}
}

View File

@ -12,73 +12,75 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)"> <core-swipe-navigation [manager]="entries">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
</ion-refresher> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<ng-container *ngIf="entry && loaded"> <ng-container *ngIf="entry && loaded">
<ion-item class="ion-text-wrap" *ngIf="showAuthor"> <ion-item class="ion-text-wrap" *ngIf="showAuthor">
<core-user-avatar [user]="entry" slot="start"></core-user-avatar> <core-user-avatar [user]="entry" slot="start"></core-user-avatar>
<ion-label> <ion-label>
<h2> <h2>
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId" <core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
[courseId]="courseId"> [courseId]="courseId">
</core-format-text>
</h2>
<p>{{ entry.userfullname }}</p>
</ion-label>
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
<ion-label>
<p class="item-heading">
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId">
</core-format-text>
</p>
</ion-label>
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"
contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId">
</core-format-text> </core-format-text>
</h2> </ion-label>
<p>{{ entry.userfullname }}</p> </ion-item>
</ion-label> <div *ngIf="entry.attachment" lines="none">
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note> <core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
</ion-item> </core-file>
<ion-item class="ion-text-wrap" *ngIf="!showAuthor"> </div>
<ion-label> <ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
<p class="item-heading"> <ion-label>
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"> <div slot="start">{{ 'core.tag.tags' | translate }}:</div>
</core-format-text> <core-tag-list [tags]="entry.tags"></core-tag-list>
</p> </ion-label>
</ion-label> </ion-item>
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note> <ion-item class="ion-text-wrap" *ngIf="!entry.approved">
</ion-item> <ion-label>
<ion-item class="ion-text-wrap"> <p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
<ion-label> </ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition" contextLevel="module" </ion-item>
[contextInstanceId]="componentId" [courseId]="courseId"> <core-comments *ngIf="glossary && glossary.allowcomments && entry && entry.id > 0 && commentsEnabled" contextLevel="module"
</core-format-text> [instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="entry.id" area="glossary_entry"
</ion-label> [courseId]="glossary.course" [showItem]="true">
</ion-item> </core-comments>
<div *ngIf="entry.attachment" lines="none"> <core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId"> [instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.course"
</core-file> [aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
</div> </core-rating-rate>
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0"> <core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
<ion-label> [instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course"
<div slot="start">{{ 'core.tag.tags' | translate }}:</div> [aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale">
<core-tag-list [tags]="entry.tags"></core-tag-list> </core-rating-aggregate>
</ion-label> </ng-container>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="!entry.approved">
<ion-label>
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
</ion-label>
</ion-item>
<core-comments *ngIf="glossary && glossary.allowcomments && entry && entry.id > 0 && commentsEnabled" contextLevel="module"
[instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="entry.id" area="glossary_entry"
[courseId]="glossary.course" [showItem]="true">
</core-comments>
<core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.course"
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
</core-rating-rate>
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course" [aggregateMethod]="glossary.assessed"
[scaleId]="glossary.scale">
</core-rating-aggregate>
</ng-container>
<ion-card *ngIf="!entry" class="core-warning-card"> <ion-card *ngIf="!entry" class="core-warning-card">
<ion-item> <ion-item>
<ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label> <ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label>
</ion-item> </ion-item>
</ion-card> </ion-card>
</core-loading> </core-loading>
</core-swipe-navigation>
</ion-content> </ion-content>

View File

@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments'; import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
import { CoreComments } from '@features/comments/services/comments'; import { CoreComments } from '@features/comments/services/comments';
import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreRatingInfo } from '@features/rating/services/rating';
@ -21,6 +23,8 @@ import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
import { import {
AddonModGlossary, AddonModGlossary,
AddonModGlossaryEntry, AddonModGlossaryEntry,
@ -35,13 +39,14 @@ import {
selector: 'page-addon-mod-glossary-entry', selector: 'page-addon-mod-glossary-entry',
templateUrl: 'entry.html', templateUrl: 'entry.html',
}) })
export class AddonModGlossaryEntryPage implements OnInit { export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent; @ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
component = AddonModGlossaryProvider.COMPONENT; component = AddonModGlossaryProvider.COMPONENT;
componentId?: number; componentId?: number;
entry?: AddonModGlossaryEntry; entry?: AddonModGlossaryEntry;
entries?: AddonModGlossaryEntryEntriesSwipeManager;
glossary?: AddonModGlossaryGlossary; glossary?: AddonModGlossaryGlossary;
loaded = false; loaded = false;
showAuthor = false; showAuthor = false;
@ -53,15 +58,30 @@ export class AddonModGlossaryEntryPage implements OnInit {
protected entryId!: number; protected entryId!: number;
constructor(protected route: ActivatedRoute) {}
/** /**
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
try { try {
const routeData = this.route.snapshot.data;
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId'); this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId');
this.tagsEnabled = CoreTag.areTagsAvailableInSite(); this.tagsEnabled = CoreTag.areTagsAvailableInSite();
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite(); this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
if (routeData.swipeEnabled ?? true) {
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
AddonModGlossaryEntriesSource,
[this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
);
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
await this.entries.start();
}
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModal(error); CoreDomUtils.showErrorModal(error);
@ -73,16 +93,23 @@ export class AddonModGlossaryEntryPage implements OnInit {
try { try {
await this.fetchEntry(); await this.fetchEntry();
if (!this.glossary) { if (!this.glossary || !this.componentId) {
return; return;
} }
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId!, this.glossary.name)); await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId, this.glossary.name));
} finally { } finally {
this.loaded = true; this.loaded = true;
} }
} }
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.entries?.destroy();
}
/** /**
* Refresh the data. * Refresh the data.
* *
@ -152,3 +179,17 @@ export class AddonModGlossaryEntryPage implements OnInit {
} }
} }
/**
* Helper to manage swiping within a collection of glossary entries.
*/
class AddonModGlossaryEntryEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
/**
* @inheritdoc
*/
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${route.params.entryId}`;
}
}

View File

@ -37,9 +37,9 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
return args.map(argument => String(argument)).join('-'); return args.map(argument => String(argument)).join('-');
} }
private items: Item[] | null = null; protected items: Item[] | null = null;
private hasMoreItems = true; protected hasMoreItems = true;
private listeners: CoreItemsListSourceListener<Item>[] = []; protected listeners: CoreItemsListSourceListener<Item>[] = [];
/** /**
* Check whether any page has been loaded. * Check whether any page has been loaded.