diff --git a/src/addons/mod/glossary/classes/glossary-entries-source.ts b/src/addons/mod/glossary/classes/glossary-entries-source.ts new file mode 100644 index 000000000..42fbe67c5 --- /dev/null +++ b/src/addons/mod/glossary/classes/glossary-entries-source.ts @@ -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 { + + 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; + + 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 { + this.glossary = await AddonModGlossary.getGlossary(this.COURSE_ID, this.CM_ID); + } + + /** + * Invalidate glossary cache. + */ + async invalidateCache(): Promise { + 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'; diff --git a/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts b/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts new file mode 100644 index 000000000..45015a760 --- /dev/null +++ b/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts @@ -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 { + + /** + * @inheritdoc + */ + async navigateToNextItem(): Promise { + 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 { + let delta = 1; + const item = await this.getItemBy(1); + + if (item && this.getSource().isNewEntryForm(item)) { + delta++; + } + + await this.navigateToItemBy(delta, 'forward'); + } + +} diff --git a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html index b0095c394..c8a816a25 100644 --- a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -54,7 +54,7 @@ [component]="component" [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline || hasOfflineRatings"> - +

{{ 'addon.mod_glossary.entriestobesynced' | translate }}

@@ -70,7 +70,7 @@
- + @@ -88,11 +88,11 @@ - - + diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 41db3e7d1..1e5c4d361 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -14,8 +14,9 @@ import { ContextLevel } from '@/core/constants'; import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; -import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { ActivatedRoute } from '@angular/router'; +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 { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; 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 { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { + AddonModGlossaryEntriesSource, + AddonModGlossaryEntryItem, + AddonModGlossaryFetchMode, +} from '../../classes/glossary-entries-source'; import { AddonModGlossary, AddonModGlossaryEntry, AddonModGlossaryEntryWithCategory, - AddonModGlossaryGetEntriesOptions, - AddonModGlossaryGetEntriesWSResponse, AddonModGlossaryGlossary, AddonModGlossaryProvider, } from '../../services/glossary'; -import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../../services/glossary-offline'; +import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline'; import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider, @@ -63,23 +67,17 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity component = AddonModGlossaryProvider.COMPONENT; moduleName = 'glossary'; - isSearch = false; - hasSearched = false; canAdd = false; loadMoreError = false; - loadingMessage?: string; - entries: AddonModGlossaryEntriesManager; + loadingMessage: string; + entries!: AddonModGlossaryEntriesManager; hasOfflineRatings = false; - glossary?: AddonModGlossaryGlossary; protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED; - protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse; - protected fetchInvalidate?: () => Promise; protected addEntryObserver?: CoreEventObserver; - protected fetchMode?: AddonModGlossaryFetchMode; - protected viewMode?: string; protected fetchedEntriesCanLoadMore = false; protected fetchedEntries: AddonModGlossaryEntry[] = []; + protected sourceUnsubscribe?: () => void; protected ratingOfflineObserver?: CoreEventObserver; protected ratingSyncObserver?: CoreEventObserver; @@ -87,26 +85,47 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false; constructor( - route: ActivatedRoute, + protected route: ActivatedRoute, protected content?: IonContent, - @Optional() courseContentsPage?: CoreCourseContentsPage, + @Optional() protected courseContentsPage?: CoreCourseContentsPage, ) { super('AddonModGlossaryIndexComponent', content, courseContentsPage); - this.entries = new AddonModGlossaryEntriesManager( - route.component, - this, - courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : '', - ); + this.loadingMessage = Translate.instant('core.loading'); + } + + 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 */ async ngOnInit(): Promise { - 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. this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => { @@ -143,11 +162,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity return; } - this.entries.start(this.splitView); + await this.entries.start(this.splitView); try { - await AddonModGlossary.logView(this.glossary.id, this.viewMode!, this.glossary.name); - CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); } catch (error) { // Ignore errors. @@ -159,14 +176,18 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity */ protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { 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.canAdd = !!this.glossary.canaddentry || false; this.dataRetrieved.emit(this.glossary); - if (!this.fetchMode) { + if (!this.entries.getSource().fetchMode) { this.switchMode('letter_all'); } @@ -177,7 +198,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity const [hasOfflineRatings] = await Promise.all([ CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule), - this.fetchEntries(), + refresh ? this.entries.reload() : this.entries.loadNextPage(), ]); 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 { - 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 */ protected async invalidateContent(): Promise { - const promises: Promise[] = []; - - 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); + await this.entries.getSource().invalidateCache(); } /** @@ -277,109 +250,50 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * @param mode New mode. */ protected switchMode(mode: AddonModGlossaryFetchMode): void { - this.fetchMode = mode; - this.isSearch = false; + this.entries.getSource().switchMode(mode); 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', - ); this.getDivider = (entry) => entry.userfullname; this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid; break; - case 'cat_all': + 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, - ); - this.getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || ''; - this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous); + const getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || ''; + + this.getDivider = getDivider; + this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous); 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', - ); this.getDivider = undefined; this.showDivider = () => false; 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', - ); this.getDivider = undefined; this.showDivider = () => false; break; case 'letter_all': - default: + 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', - ); - this.getDivider = (entry) => { + const getDivider = (entry) => { // Try to get the first letter without HTML tags. const noTags = CoreTextUtils.cleanTags(entry.concept); 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; + } } } @@ -391,7 +305,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity */ async loadMoreEntries(infiniteComplete?: () => void): Promise { try { - await this.fetchEntries(true); + this.loadMoreError = false; + + await this.entries.loadNextPage(); } catch (error) { this.loadMoreError = true; CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true); @@ -406,21 +322,34 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * @param event Event. */ async openModePicker(event: MouseEvent): Promise { - const mode = await CoreDomUtils.openPopover({ + if (!this.glossary) { + return; + } + + const previousMode = this.entries.getSource().fetchMode; + const newMode = await CoreDomUtils.openPopover({ component: AddonModGlossaryModePickerPopoverComponent, componentProps: { - browseModes: this.glossary!.browsemodes, - selectedMode: this.isSearch ? '' : this.fetchMode, + browseModes: this.glossary.browsemodes, + selectedMode: this.isSearch ? '' : previousMode, }, event, }); - if (mode) { - if (mode !== this.fetchMode) { - this.changeFetchMode(mode); - } else if (this.isSearch) { - this.toggleSearch(); - } + if (!newMode) { + return; + } + + if (newMode !== previousMode) { + this.changeFetchMode(newMode); + + return; + } + + if (this.isSearch) { + this.toggleSearch(); + + return; } } @@ -429,20 +358,22 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity */ toggleSearch(): void { if (this.isSearch) { - this.isSearch = false; - 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; + const fetchMode = this.entries.getSource().fetchMode; - this.fetchedEntries = this.entries.onlineEntries; - this.fetchedEntriesCanLoadMore = !this.entries.completed; - this.entries.setItems([], false); + fetchMode && this.switchMode(fetchMode); + this.entries.getSource().stopSearch(this.fetchedEntries, this.fetchedEntriesCanLoadMore); + + 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. */ changeFetchMode(mode: AddonModGlossaryFetchMode): void { - this.isSearch = false; this.loadingMessage = Translate.instant('core.loading'); this.content?.scrollToTop(); this.switchMode(mode); @@ -463,7 +393,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * Opens new entry editor. */ 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 { 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.hasSearched = true; + + this.entries.getSource().search(query); this.loadContent(); } @@ -503,154 +418,44 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.addEntryObserver?.off(); this.ratingOfflineObserver?.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. */ -class AddonModGlossaryEntriesManager extends CorePageItemsListManager { +class AddonModGlossaryEntriesManager extends CoreListItemsManager { - onlineEntries: AddonModGlossaryEntry[] = []; - offlineEntries: AddonModGlossaryOfflineEntry[] = []; - - protected glossaryPathPrefix: string; - protected component: AddonModGlossaryIndexComponent; - - constructor( - pageComponent: unknown, - component: AddonModGlossaryIndexComponent, - glossaryPathPrefix: string, - ) { - super(pageComponent); - - this.component = component; - this.glossaryPathPrefix = glossaryPathPrefix; + get offlineEntries(): AddonModGlossaryOfflineEntry[] { + return this.getSource().offlineEntries; } - /** - * Type guard to infer NewEntryForm objects. - * - * @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(( this.offlineEntries).concat(onlineEntries), hasMoreItems); - } - - /** - * Update offline entries items. - * - * @param offlineEntries Offline entries. - */ - setOfflineEntries(offlineEntries: AddonModGlossaryOfflineEntry[]): void { - this.setItems(( offlineEntries).concat(this.onlineEntries), this.hasMoreItems); + get onlineEntries(): AddonModGlossaryEntry[] { + return this.getSource().onlineEntries; } /** * @inheritdoc */ - setItems(entries: EntryItem[], hasMoreItems: boolean = false): void { - super.setItems(entries, hasMoreItems); - - 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); - } - }); + protected getDefaultItem(): AddonModGlossaryEntryItem | null { + return this.getSource().onlineEntries[0] || null; } /** * @inheritdoc */ - resetItems(): void { - super.resetItems(); - this.onlineEntries = []; - this.offlineEntries = []; - } + protected async logActivity(): Promise { + const glossary = this.getSource().glossary; + const viewMode = this.getSource().viewMode; - /** - * @inheritdoc - */ - protected getItemPath(entry: EntryItem): string { - if (this.isOnlineEntry(entry)) { - return `${this.glossaryPathPrefix}entry/${entry.id}`; + if (!glossary || !viewMode) { + return; } - if (this.isOfflineEntry(entry)) { - 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; + await AddonModGlossary.logView(glossary.id, viewMode, glossary.name); } } - -export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all'; diff --git a/src/addons/mod/glossary/components/mode-picker/mode-picker.ts b/src/addons/mod/glossary/components/mode-picker/mode-picker.ts index e3e08071b..d808d3b76 100644 --- a/src/addons/mod/glossary/components/mode-picker/mode-picker.ts +++ b/src/addons/mod/glossary/components/mode-picker/mode-picker.ts @@ -14,7 +14,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { PopoverController } from '@singletons'; -import { AddonModGlossaryFetchMode } from '../index'; +import { AddonModGlossaryFetchMode } from '../../classes/glossary-entries-source'; /** * Component to display the mode picker. diff --git a/src/addons/mod/glossary/glossary.module.ts b/src/addons/mod/glossary/glossary.module.ts index 9a4b09793..1b3e27e42 100644 --- a/src/addons/mod/glossary/glossary.module.ts +++ b/src/addons/mod/glossary/glossary.module.ts @@ -51,10 +51,12 @@ const mainMenuRoutes: Routes = [ { path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), + data: { swipeEnabled: false }, }, { path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), + data: { swipeEnabled: false }, }, { path: AddonModGlossaryModuleHandlerService.PAGE_NAME, @@ -65,10 +67,12 @@ const mainMenuRoutes: Routes = [ { path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), + data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, }, { path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), + data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, }, ], () => CoreScreen.isMobile, @@ -80,10 +84,12 @@ const courseContentsRoutes: Routes = conditionalRoutes( { path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), + data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, }, { path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), + data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, }, ], () => CoreScreen.isTablet, diff --git a/src/addons/mod/glossary/pages/edit/edit.html b/src/addons/mod/glossary/pages/edit/edit.html index 55faf51fe..4b195ebab 100644 --- a/src/addons/mod/glossary/pages/edit/edit.html +++ b/src/addons/mod/glossary/pages/edit/edit.html @@ -12,72 +12,75 @@ - -
- - {{ 'addon.mod_glossary.concept' | translate }} - - - - - {{ 'addon.mod_glossary.definition' | translate }} - - - - - - {{ 'addon.mod_glossary.categories' | translate }} - - - - {{ category.name }} - - - - - - {{ 'addon.mod_glossary.aliases' | translate }} - - - - - - -

{{ 'addon.mod_glossary.attachment' | translate }}

-
-
- - - + + + + + {{ 'addon.mod_glossary.concept' | translate }} + + + + + {{ 'addon.mod_glossary.definition' | translate }} + + + + + + {{ 'addon.mod_glossary.categories' | translate }} + + + + {{ category.name }} + + + + + + {{ 'addon.mod_glossary.aliases' | translate }} + + + + -

{{ 'addon.mod_glossary.linking' | translate }}

+

{{ 'addon.mod_glossary.attachment' | translate }}

- - {{ 'addon.mod_glossary.entryusedynalink' | translate }} - - - - {{ 'addon.mod_glossary.casesensitive' | translate }} - - - - - {{ 'addon.mod_glossary.fullmatch' | translate }} - - -
- - {{ 'core.save' | translate }} - -
-
+ + + + + +

{{ 'addon.mod_glossary.linking' | translate }}

+
+
+ + {{ 'addon.mod_glossary.entryusedynalink' | translate }} + + + + {{ 'addon.mod_glossary.casesensitive' | translate }} + + + + + {{ 'addon.mod_glossary.fullmatch' | translate }} + + +
+ + {{ 'core.save' | translate }} + + + +
diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index 33f384fc3..dfb06aa5d 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // 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 { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; 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 { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CanLeave } from '@guards/can-leave'; @@ -26,6 +28,8 @@ import { CoreTextUtils } from '@services/utils/text'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreForms } from '@singletons/form'; +import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source'; +import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager'; import { AddonModGlossary, AddonModGlossaryCategory, @@ -45,7 +49,7 @@ import { AddonModGlossaryOffline } from '../../services/glossary-offline'; selector: 'page-addon-mod-glossary-edit', templateUrl: 'edit.html', }) -export class AddonModGlossaryEditPage implements OnInit, CanLeave { +export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { @ViewChild('editFormEl') formElement?: ElementRef; @@ -64,6 +68,8 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { timecreated: 0, }; + entries?: AddonModGlossaryEditEntriesSwipeManager; + options = { categories: [], aliases: '', @@ -80,18 +86,30 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { protected originalData?: AddonModGlossaryNewEntryWithFiles; protected saved = false; - constructor(@Optional() protected splitView: CoreSplitViewComponent) {} + constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} /** * Component being initialized. */ - ngOnInit(): void { + async ngOnInit(): Promise { try { + const routeData = this.route.snapshot.data; this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated'); this.concept = CoreNavigator.getRouteParam('concept') || ''; 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) { CoreDomUtils.showErrorModal(error); @@ -103,6 +121,13 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { this.fetchData(); } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.entries?.destroy(); + } + /** * Fetch required data. * @@ -134,7 +159,11 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { * @return Promise resolved when done. */ protected async loadOfflineData(): Promise { - 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.definition = entry.definition || ''; @@ -159,7 +188,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { // Treat offline attachments if any. 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(); } @@ -236,6 +265,10 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { definition = CoreTextUtils.formatHtmlLines(definition); try { + if (!this.glossary) { + return; + } + // Upload attachments first if any. const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated); @@ -244,7 +277,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { categories: this.options.categories.join(','), }; - if (this.glossary!.usedynalink) { + if (this.glossary.usedynalink) { options.usedynalink = this.options.usedynalink ? 1 : 0; if (this.options.usedynalink) { options.casesensitive = this.options.casesensitive ? 1 : 0; @@ -253,9 +286,9 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { } 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. - 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, cmId: this.cmId, }); @@ -268,7 +301,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { // Save entry in offline. await AddonModGlossaryOffline.addNewEntry( - this.glossary!.id, + this.glossary.id, this.entry.concept, definition, this.courseId, @@ -283,7 +316,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { // Try to send it to server. // Don't allow offline if there are attachments since they were uploaded fine. await AddonModGlossary.addEntry( - this.glossary!.id, + this.glossary.id, this.entry.concept, definition, this.courseId, @@ -293,7 +326,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { timeCreated: timecreated, discardEntry: this.entry, allowOffline: !this.attachments.length, - checkDuplicates: !this.glossary!.allowduplicatedentries, + checkDuplicates: !this.glossary.allowduplicatedentries, }, ); } @@ -303,12 +336,12 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { if (entryId) { // 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(AddonModGlossaryProvider.ADD_ENTRY_EVENT, { - glossaryId: this.glossary!.id, + glossaryId: this.glossary.id, entryId: entryId, }, CoreSites.getCurrentSiteId()); @@ -342,7 +375,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { protected async uploadAttachments( timecreated: number, ): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> { - if (!this.attachments.length) { + if (!this.attachments.length || !this.glossary) { return { saveOffline: false, }; @@ -352,7 +385,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles( this.attachments, AddonModGlossaryProvider.COMPONENT, - this.glossary!.id, + this.glossary.id, ); return { @@ -362,7 +395,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { } catch { // Cannot upload them in online, save them in offline. const attachmentsResult = await AddonModGlossaryHelper.storeFiles( - this.glossary!.id, + this.glossary.id, this.entry.concept, timecreated, 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}`; + } + +} diff --git a/src/addons/mod/glossary/pages/entry/entry.html b/src/addons/mod/glossary/pages/entry/entry.html index 03737a4a1..e5d870d40 100644 --- a/src/addons/mod/glossary/pages/entry/entry.html +++ b/src/addons/mod/glossary/pages/entry/entry.html @@ -12,73 +12,75 @@ - - - + + + + - - - - - -

- + + + + + +

+ + +

+

{{ entry.userfullname }}

+
+ {{ entry.timemodified | coreDateDayOrTime }} +
+ + +

+ + +

+
+ {{ entry.timemodified | coreDateDayOrTime }} +
+ + + -

-

{{ entry.userfullname }}

-
- {{ entry.timemodified | coreDateDayOrTime }} -
- - -

- - -

-
- {{ entry.timemodified | coreDateDayOrTime }} -
- - - - - - -
- - -
- - -
{{ 'core.tag.tags' | translate }}:
- -
-
- - -

{{ 'addon.mod_glossary.entrypendingapproval' | translate }}

-
-
- - - - - - -
+ + +
+ + +
+ + +
{{ 'core.tag.tags' | translate }}:
+ +
+
+ + +

{{ 'addon.mod_glossary.entrypendingapproval' | translate }}

+
+
+ + + + + + + - - - {{ 'addon.mod_glossary.errorloadingentry' | translate }} - - -
+ + + {{ 'addon.mod_glossary.errorloadingentry' | translate }} + + + +
diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts index ae3c836e4..d88b470c1 100644 --- a/src/addons/mod/glossary/pages/entry/entry.ts +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // 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 { CoreComments } from '@features/comments/services/comments'; import { CoreRatingInfo } from '@features/rating/services/rating'; @@ -21,6 +23,8 @@ import { IonRefresher } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source'; +import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager'; import { AddonModGlossary, AddonModGlossaryEntry, @@ -35,13 +39,14 @@ import { selector: 'page-addon-mod-glossary-entry', templateUrl: 'entry.html', }) -export class AddonModGlossaryEntryPage implements OnInit { +export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { @ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent; component = AddonModGlossaryProvider.COMPONENT; componentId?: number; entry?: AddonModGlossaryEntry; + entries?: AddonModGlossaryEntryEntriesSwipeManager; glossary?: AddonModGlossaryGlossary; loaded = false; showAuthor = false; @@ -53,15 +58,30 @@ export class AddonModGlossaryEntryPage implements OnInit { protected entryId!: number; + constructor(protected route: ActivatedRoute) {} + /** * @inheritdoc */ async ngOnInit(): Promise { try { + const routeData = this.route.snapshot.data; this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId'); this.tagsEnabled = CoreTag.areTagsAvailableInSite(); 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) { CoreDomUtils.showErrorModal(error); @@ -73,16 +93,23 @@ export class AddonModGlossaryEntryPage implements OnInit { try { await this.fetchEntry(); - if (!this.glossary) { + if (!this.glossary || !this.componentId) { 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 { this.loaded = true; } } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.entries?.destroy(); + } + /** * 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}`; + } + +} diff --git a/src/core/classes/items-management/items-manager-source.ts b/src/core/classes/items-management/items-manager-source.ts index 5a00e5c9c..e512747ec 100644 --- a/src/core/classes/items-management/items-manager-source.ts +++ b/src/core/classes/items-management/items-manager-source.ts @@ -37,9 +37,9 @@ export abstract class CoreItemsManagerSource { return args.map(argument => String(argument)).join('-'); } - private items: Item[] | null = null; - private hasMoreItems = true; - private listeners: CoreItemsListSourceListener[] = []; + protected items: Item[] | null = null; + protected hasMoreItems = true; + protected listeners: CoreItemsListSourceListener[] = []; /** * Check whether any page has been loaded.