MOBILE-3926 glossary: Entries swipe navigation
parent
e8d0026995
commit
70ff7a375a
|
@ -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';
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
|
@ -54,7 +54,7 @@
|
|||
[component]="component" [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline || hasOfflineRatings">
|
||||
</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-label>
|
||||
<h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
|
||||
|
@ -70,7 +70,7 @@
|
|||
</ion-item>
|
||||
</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">
|
||||
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
|
||||
<ion-label>
|
||||
|
@ -88,11 +88,11 @@
|
|||
</ng-container>
|
||||
</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">
|
||||
</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-loading>
|
||||
|
||||
|
|
|
@ -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<void>;
|
||||
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<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.
|
||||
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<void> {
|
||||
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<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
|
||||
*/
|
||||
protected async invalidateContent(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
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<void> {
|
||||
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<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,
|
||||
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<EntryItem> {
|
||||
class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
|
||||
|
||||
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((<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);
|
||||
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<void> {
|
||||
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';
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -12,72 +12,75 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<form #editFormEl *ngIf="glossary">
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
|
||||
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept" name="concept">
|
||||
</ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
|
||||
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
|
||||
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
|
||||
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId" elementId="definition_editor"
|
||||
[draftExtraParams]="editorExtraParams">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="categories.length > 0">
|
||||
<ion-label position="stacked" id="addon-mod-glossary-categories-label">
|
||||
{{ 'addon.mod_glossary.categories' | translate }}
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label"
|
||||
interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
|
||||
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
|
||||
<ion-select-option *ngFor="let category of categories" [value]="category.id">
|
||||
{{ category.name }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked" id="addon-mod-glossary-aliases-label">
|
||||
{{ 'addon.mod_glossary.aliases' | translate }}
|
||||
</ion-label>
|
||||
<ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases"
|
||||
aria-labelledby="addon-mod-glossary-aliases-label" name="aliases">
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<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">
|
||||
<core-swipe-navigation [manager]="entries">
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<form #editFormEl *ngIf="glossary">
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
|
||||
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept"
|
||||
name="concept">
|
||||
</ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
|
||||
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
|
||||
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
|
||||
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId"
|
||||
elementId="definition_editor" [draftExtraParams]="editorExtraParams">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="categories.length > 0">
|
||||
<ion-label position="stacked" id="addon-mod-glossary-categories-label">
|
||||
{{ 'addon.mod_glossary.categories' | translate }}
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label"
|
||||
interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
|
||||
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
|
||||
<ion-select-option *ngFor="let category of categories" [value]="category.id">
|
||||
{{ category.name }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked" id="addon-mod-glossary-aliases-label">
|
||||
{{ 'addon.mod_glossary.aliases' | translate }}
|
||||
</ion-label>
|
||||
<ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases"
|
||||
aria-labelledby="addon-mod-glossary-aliases-label" name="aliases">
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
|
||||
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
|
||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<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-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
|
||||
[courseId]="courseId">
|
||||
</core-attachments>
|
||||
<ng-container *ngIf="glossary.usedynalink">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
|
||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<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>
|
||||
|
|
|
@ -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: <string[]> [],
|
||||
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<void> {
|
||||
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<string>('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<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.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}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,73 +12,75 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-swipe-navigation [manager]="entries">
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ng-container *ngIf="entry && loaded">
|
||||
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
||||
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
|
||||
[courseId]="courseId">
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ng-container *ngIf="entry && loaded">
|
||||
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
||||
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
|
||||
[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>
|
||||
</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>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div *ngIf="entry.attachment" lines="none">
|
||||
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
||||
</core-file>
|
||||
</div>
|
||||
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
|
||||
<ion-label>
|
||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||
<core-tag-list [tags]="entry.tags"></core-tag-list>
|
||||
</ion-label>
|
||||
</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-label>
|
||||
</ion-item>
|
||||
<div *ngIf="entry.attachment" lines="none">
|
||||
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
||||
</core-file>
|
||||
</div>
|
||||
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
|
||||
<ion-label>
|
||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||
<core-tag-list [tags]="entry.tags"></core-tag-list>
|
||||
</ion-label>
|
||||
</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-item>
|
||||
<ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</core-loading>
|
||||
<ion-card *ngIf="!entry" class="core-warning-card">
|
||||
<ion-item>
|
||||
<ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</core-loading>
|
||||
</core-swipe-navigation>
|
||||
</ion-content>
|
||||
|
|
|
@ -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<void> {
|
||||
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}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,9 +37,9 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
return args.map(argument => String(argument)).join('-');
|
||||
}
|
||||
|
||||
private items: Item[] | null = null;
|
||||
private hasMoreItems = true;
|
||||
private listeners: CoreItemsListSourceListener<Item>[] = [];
|
||||
protected items: Item[] | null = null;
|
||||
protected hasMoreItems = true;
|
||||
protected listeners: CoreItemsListSourceListener<Item>[] = [];
|
||||
|
||||
/**
|
||||
* Check whether any page has been loaded.
|
||||
|
|
Loading…
Reference in New Issue