377 lines
14 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 { AddonModGlossaryHelper } from '@addons/mod/glossary/services/glossary-helper';
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '@addons/mod/glossary/services/glossary-offline';
import { Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
import { CoreComments } from '@features/comments/services/comments';
import { CoreRatingInfo } from '@features/rating/services/rating';
import { CoreTag } from '@features/tag/services/tag';
import { FileEntry } from '@awesome-cordova-plugins/file/ngx';
import { CoreNavigator } from '@services/navigator';
import { CoreNetwork } from '@services/network';
import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from '../../classes/glossary-entries-source';
import {
AddonModGlossary,
AddonModGlossaryEntry,
AddonModGlossaryGlossary,
AddonModGlossaryProvider,
GLOSSARY_ENTRY_UPDATED,
} from '../../services/glossary';
import { CoreTime } from '@singletons/time';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
/**
* Page that displays a glossary entry.
*/
@Component({
selector: 'page-addon-mod-glossary-entry',
templateUrl: 'entry.html',
})
export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
component = AddonModGlossaryProvider.COMPONENT;
componentId?: number;
onlineEntry?: AddonModGlossaryEntry;
offlineEntry?: AddonModGlossaryOfflineEntry;
offlineEntryFiles?: FileEntry[];
entries!: AddonModGlossaryEntryEntriesSwipeManager;
glossary?: AddonModGlossaryGlossary;
entryUpdatedObserver?: CoreEventObserver;
loaded = false;
showAuthor = false;
showDate = false;
ratingInfo?: CoreRatingInfo;
tagsEnabled = false;
canEdit = false;
canDelete = false;
commentsEnabled = false;
courseId!: number;
cmId!: number;
protected logView: () => void;
constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) {
this.logView = CoreTime.once(async () => {
if (!this.onlineEntry || !this.glossary || !this.componentId) {
return;
}
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.onlineEntry.id, this.componentId));
this.analyticsLogEvent('mod_glossary_get_entry_by_id', `/mod/glossary/showentry.php?eid=${this.onlineEntry.id}`);
});
}
get entry(): AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | undefined {
return this.onlineEntry ?? this.offlineEntry;
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
let onlineEntryId: number | null = null;
let offlineEntryTimeCreated: number | null = null;
try {
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
this.commentsEnabled = CoreComments.areCommentsEnabledInSite();
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
const entrySlug = CoreNavigator.getRequiredRouteParam<string>('entrySlug');
const routeData = this.route.snapshot.data;
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
AddonModGlossaryEntriesSource,
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
);
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
await this.entries.start();
if (entrySlug.startsWith('new-')) {
offlineEntryTimeCreated = Number(entrySlug.slice(4));
} else {
onlineEntryId = Number(entrySlug);
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
return;
}
this.entryUpdatedObserver = CoreEvents.on(GLOSSARY_ENTRY_UPDATED, data => {
if (data.glossaryId !== this.glossary?.id) {
return;
}
if (
(this.onlineEntry && this.onlineEntry.id === data.entryId) ||
(this.offlineEntry && this.offlineEntry.timecreated === data.timecreated)
) {
this.doRefresh();
}
});
try {
if (onlineEntryId) {
await this.loadOnlineEntry(onlineEntryId);
} else if (offlineEntryTimeCreated) {
await this.loadOfflineEntry(offlineEntryTimeCreated);
}
} finally {
this.loaded = true;
}
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.entries.destroy();
this.entryUpdatedObserver?.off();
}
/**
* Edit entry.
*/
async editEntry(): Promise<void> {
await CoreNavigator.navigate('./edit');
}
/**
* Delete entry.
*/
async deleteEntry(): Promise<void> {
// Log analytics even if the user cancels for consistency with LMS.
this.analyticsLogEvent(
'mod_glossary_delete_entry',
`/mod/glossary/deleteentry.php?id=${this.glossary?.id}&mode=delete&entry=${this.onlineEntry?.id}`,
);
const glossaryId = this.glossary?.id;
const cancelled = await CoreUtils.promiseFails(
CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')),
);
if (!glossaryId || cancelled) {
return;
}
const modal = await CoreDomUtils.showModalLoading();
try {
if (this.onlineEntry) {
const entryId = this.onlineEntry.id;
await AddonModGlossary.deleteEntry(glossaryId, entryId);
await Promise.all([
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(entryId)),
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByLetter(glossaryId)),
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByAuthor(glossaryId)),
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId)),
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION')),
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE')),
CoreUtils.ignoreErrors(this.entries.getSource().invalidateCache(false)),
]);
} else if (this.offlineEntry) {
const concept = this.offlineEntry.concept;
const timecreated = this.offlineEntry.timecreated;
await AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timecreated);
await AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timecreated);
}
CoreDomUtils.showToast('addon.mod_glossary.entrydeleted', true, ToastDuration.LONG);
if (this.splitView?.outletActivated) {
await CoreNavigator.navigate('../../');
} else {
await CoreNavigator.back();
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errordeleting', true);
} finally {
modal.dismiss();
}
}
/**
* Refresh the data.
*
* @param refresher Refresher.
* @returns Promise resolved when done.
*/
async doRefresh(refresher?: HTMLIonRefresherElement): Promise<void> {
if (this.onlineEntry && this.glossary?.allowcomments && this.onlineEntry.id > 0 && this.commentsEnabled && this.comments) {
// Refresh comments asynchronously (without blocking the current promise).
CoreUtils.ignoreErrors(this.comments.doRefresh());
}
try {
if (this.onlineEntry) {
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(this.onlineEntry.id));
await this.loadOnlineEntry(this.onlineEntry.id);
} else if (this.offlineEntry) {
const entrySlug = CoreNavigator.getRequiredRouteParam<string>('entrySlug');
const timecreated = Number(entrySlug.slice(4));
await this.loadOfflineEntry(timecreated);
}
} finally {
refresher?.complete();
}
}
/**
* Load online entry data.
*/
protected async loadOnlineEntry(entryId: number): Promise<void> {
try {
const result = await AddonModGlossary.getEntry(entryId);
const canDeleteEntries = CoreNetwork.isOnline() && await AddonModGlossary.canDeleteEntries();
const canUpdateEntries = CoreNetwork.isOnline() && await AddonModGlossary.canUpdateEntries();
this.onlineEntry = result.entry;
this.ratingInfo = result.ratinginfo;
this.canDelete = canDeleteEntries && !!result.permissions?.candelete;
this.canEdit = canUpdateEntries && !!result.permissions?.canupdate;
await this.loadGlossary();
this.logView();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
}
}
/**
* Load offline entry data.
*
* @param timecreated Entry Timecreated.
*/
protected async loadOfflineEntry(timecreated: number): Promise<void> {
try {
const glossary = await this.loadGlossary();
this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, timecreated);
this.offlineEntryFiles = this.offlineEntry.attachments && this.offlineEntry.attachments.offline > 0
? await AddonModGlossaryHelper.getStoredFiles(
glossary.id,
this.offlineEntry.concept,
timecreated,
)
: undefined;
this.canEdit = true;
this.canDelete = true;
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
}
}
/**
* Load glossary data.
*
* @returns Glossary.
*/
protected async loadGlossary(): Promise<AddonModGlossaryGlossary> {
if (this.glossary) {
return this.glossary;
}
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId);
this.componentId = this.glossary.coursemodule;
switch (this.glossary.displayformat) {
case 'fullwithauthor':
case 'encyclopedia':
this.showAuthor = true;
this.showDate = true;
break;
case 'fullwithoutauthor':
this.showAuthor = false;
this.showDate = true;
break;
default: // Default, and faq, simple, entrylist, continuous.
this.showAuthor = false;
this.showDate = false;
}
return this.glossary;
}
/**
* Function called when rating is updated online.
*/
ratingUpdated(): void {
if (!this.onlineEntry) {
return;
}
AddonModGlossary.invalidateEntry(this.onlineEntry.id);
}
/**
* Log analytics event.
*
* @param wsName WS name.
* @param url URL.
*/
protected analyticsLogEvent(wsName: string, url: string): void {
if (!this.onlineEntry || !this.glossary) {
return;
}
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: wsName,
name: this.onlineEntry.concept,
data: { id: this.onlineEntry.id, glossaryid: this.glossary.id, category: 'glossary' },
url,
});
}
}
/**
* Helper to manage swiping within a collection of glossary entries.
*/
class AddonModGlossaryEntryEntriesSwipeManager
extends CoreSwipeNavigationItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
/**
* @inheritdoc
*/
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${snapshot.params.entrySlug}`;
}
}