462 lines
15 KiB
TypeScript
462 lines
15 KiB
TypeScript
// (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 { ContextLevel } from '@/core/constants';
|
|
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
|
import { ActivatedRoute } from '@angular/router';
|
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
|
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';
|
|
import { CoreCourse } from '@features/course/services/course';
|
|
import { CoreRatingProvider } from '@features/rating/services/rating';
|
|
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
|
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
|
import { IonContent } from '@ionic/angular';
|
|
import { CoreSites } from '@services/sites';
|
|
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,
|
|
AddonModGlossaryGlossary,
|
|
AddonModGlossaryProvider,
|
|
} from '../../services/glossary';
|
|
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
|
import {
|
|
AddonModGlossaryAutoSyncData,
|
|
AddonModGlossarySyncProvider,
|
|
AddonModGlossarySyncResult,
|
|
} from '../../services/glossary-sync';
|
|
import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module';
|
|
import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch';
|
|
import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker';
|
|
|
|
/**
|
|
* Component that displays a glossary entry page.
|
|
*/
|
|
@Component({
|
|
selector: 'addon-mod-glossary-index',
|
|
templateUrl: 'addon-mod-glossary-index.html',
|
|
})
|
|
export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent
|
|
implements OnInit, AfterViewInit, OnDestroy {
|
|
|
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
|
|
|
component = AddonModGlossaryProvider.COMPONENT;
|
|
moduleName = 'glossary';
|
|
|
|
canAdd = false;
|
|
loadMoreError = false;
|
|
loadingMessage: string;
|
|
entries!: AddonModGlossaryEntriesManager;
|
|
hasOfflineRatings = false;
|
|
|
|
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
|
protected addEntryObserver?: CoreEventObserver;
|
|
protected fetchedEntriesCanLoadMore = false;
|
|
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
|
protected sourceUnsubscribe?: () => void;
|
|
protected ratingOfflineObserver?: CoreEventObserver;
|
|
protected ratingSyncObserver?: CoreEventObserver;
|
|
|
|
getDivider?: (entry: AddonModGlossaryEntry) => string;
|
|
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
|
|
|
|
constructor(
|
|
protected route: ActivatedRoute,
|
|
protected content?: IonContent,
|
|
@Optional() protected courseContentsPage?: CoreCourseContentsPage,
|
|
) {
|
|
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
|
|
|
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<void> {
|
|
await super.ngOnInit();
|
|
|
|
// Initialize entries manager.
|
|
const source = CoreRoutedItemsManagerSourcesTracker.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) => {
|
|
if (this.glossary && this.glossary.id === data.glossaryId) {
|
|
this.showLoadingAndRefresh(false);
|
|
|
|
// Check completion since it could be configured to complete once the user adds a new entry.
|
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
|
}
|
|
});
|
|
|
|
// Listen for offline ratings saved and synced.
|
|
this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
|
|
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
|
&& data.instanceId == this.glossary.coursemodule) {
|
|
this.hasOfflineRatings = true;
|
|
}
|
|
});
|
|
this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
|
|
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
|
&& data.instanceId == this.glossary.coursemodule) {
|
|
this.hasOfflineRatings = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
async ngAfterViewInit(): Promise<void> {
|
|
await this.loadContent(false, true);
|
|
|
|
if (!this.glossary) {
|
|
return;
|
|
}
|
|
|
|
await this.entries.start(this.splitView);
|
|
|
|
try {
|
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
|
} catch (error) {
|
|
// Ignore errors.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
|
try {
|
|
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.entries.getSource().fetchMode) {
|
|
this.switchMode('letter_all');
|
|
}
|
|
|
|
if (sync) {
|
|
// Try to synchronize the glossary.
|
|
await this.syncActivity(showErrors);
|
|
}
|
|
|
|
const [hasOfflineRatings] = await Promise.all([
|
|
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
|
refresh ? this.entries.reload() : this.entries.load(),
|
|
]);
|
|
|
|
this.hasOfflineRatings = hasOfflineRatings;
|
|
} finally {
|
|
this.fillContextMenu(refresh);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
protected async invalidateContent(): Promise<void> {
|
|
await this.entries.getSource().invalidateCache();
|
|
}
|
|
|
|
/**
|
|
* Performs the sync of the activity.
|
|
*
|
|
* @return Promise resolved when done.
|
|
*/
|
|
protected sync(): Promise<AddonModGlossarySyncResult> {
|
|
return AddonModGlossaryPrefetchHandler.sync(this.module, this.courseId);
|
|
}
|
|
|
|
/**
|
|
* Checks if sync has succeed from result sync data.
|
|
*
|
|
* @param result Data returned on the sync function.
|
|
* @return Whether it succeed or not.
|
|
*/
|
|
protected hasSyncSucceed(result: AddonModGlossarySyncResult): boolean {
|
|
return result.updated;
|
|
}
|
|
|
|
/**
|
|
* Compares sync event data with current data to check if refresh content is needed.
|
|
*
|
|
* @param syncEventData Data receiven on sync observer.
|
|
* @return True if refresh is needed, false otherwise.
|
|
*/
|
|
protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncData): boolean {
|
|
return !!this.glossary && syncEventData.glossaryId == this.glossary.id &&
|
|
syncEventData.userId == CoreSites.getCurrentSiteUserId();
|
|
}
|
|
|
|
/**
|
|
* Change fetch mode.
|
|
*
|
|
* @param mode New mode.
|
|
*/
|
|
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
|
this.entries.getSource().switchMode(mode);
|
|
|
|
switch (mode) {
|
|
case 'author_all':
|
|
// Browse by author.
|
|
this.getDivider = (entry) => entry.userfullname;
|
|
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
|
|
break;
|
|
|
|
case 'cat_all': {
|
|
// Browse by category.
|
|
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.getDivider = undefined;
|
|
this.showDivider = () => false;
|
|
break;
|
|
|
|
case 'recently_updated':
|
|
// Recently updated.
|
|
this.getDivider = undefined;
|
|
this.showDivider = () => false;
|
|
break;
|
|
|
|
case 'letter_all':
|
|
default: {
|
|
// Consider it is 'letter_all'.
|
|
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.getDivider = getDivider;
|
|
this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience function to load more entries.
|
|
*
|
|
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
|
try {
|
|
this.loadMoreError = false;
|
|
|
|
await this.entries.load();
|
|
} catch (error) {
|
|
this.loadMoreError = true;
|
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
|
} finally {
|
|
infiniteComplete && infiniteComplete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show the mode picker menu.
|
|
*
|
|
* @param event Event.
|
|
*/
|
|
async openModePicker(event: MouseEvent): Promise<void> {
|
|
if (!this.glossary) {
|
|
return;
|
|
}
|
|
|
|
const previousMode = this.entries.getSource().fetchMode;
|
|
const newMode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
|
|
component: AddonModGlossaryModePickerPopoverComponent,
|
|
componentProps: {
|
|
browseModes: this.glossary.browsemodes,
|
|
selectedMode: this.isSearch ? '' : previousMode,
|
|
},
|
|
event,
|
|
});
|
|
|
|
if (!newMode) {
|
|
return;
|
|
}
|
|
|
|
if (newMode !== previousMode) {
|
|
this.changeFetchMode(newMode);
|
|
|
|
return;
|
|
}
|
|
|
|
if (this.isSearch) {
|
|
this.toggleSearch();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggles between search and fetch mode.
|
|
*/
|
|
toggleSearch(): void {
|
|
if (this.isSearch) {
|
|
const fetchMode = this.entries.getSource().fetchMode;
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Change fetch mode.
|
|
*
|
|
* @param mode Mode.
|
|
*/
|
|
changeFetchMode(mode: AddonModGlossaryFetchMode): void {
|
|
this.loadingMessage = Translate.instant('core.loading');
|
|
this.content?.scrollToTop();
|
|
this.switchMode(mode);
|
|
this.loaded = false;
|
|
this.loadContent();
|
|
}
|
|
|
|
/**
|
|
* Opens new entry editor.
|
|
*/
|
|
openNewEntry(): void {
|
|
this.entries.select(AddonModGlossaryEntriesSource.NEW_ENTRY);
|
|
}
|
|
|
|
/**
|
|
* Search entries.
|
|
*
|
|
* @param query Text entered on the search box.
|
|
*/
|
|
search(query: string): void {
|
|
this.loadingMessage = Translate.instant('core.searching');
|
|
this.loaded = false;
|
|
|
|
this.entries.getSource().search(query);
|
|
this.loadContent();
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ngOnDestroy(): void {
|
|
super.ngOnDestroy();
|
|
|
|
this.addEntryObserver?.off();
|
|
this.ratingOfflineObserver?.off();
|
|
this.ratingSyncObserver?.off();
|
|
this.sourceUnsubscribe?.call(null);
|
|
this.entries.destroy();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Entries manager.
|
|
*/
|
|
class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
|
|
|
|
get offlineEntries(): AddonModGlossaryOfflineEntry[] {
|
|
return this.getSource().offlineEntries;
|
|
}
|
|
|
|
get onlineEntries(): AddonModGlossaryEntry[] {
|
|
return this.getSource().onlineEntries;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
protected getDefaultItem(): AddonModGlossaryEntryItem | null {
|
|
return this.getSource().onlineEntries[0] || null;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
protected async logActivity(): Promise<void> {
|
|
const glossary = this.getSource().glossary;
|
|
const viewMode = this.getSource().viewMode;
|
|
|
|
if (!glossary || !viewMode) {
|
|
return;
|
|
}
|
|
|
|
await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
|
|
}
|
|
|
|
}
|