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">
|
[component]="component" [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline || hasOfflineRatings">
|
||||||
</core-course-module-info>
|
</core-course-module-info>
|
||||||
|
|
||||||
<ion-list *ngIf="!isSearch && entries.offlineEntries.length > 0">
|
<ion-list *ngIf="!isSearch && entries && entries.offlineEntries.length > 0">
|
||||||
<ion-item-divider>
|
<ion-item-divider>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
|
<h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
<ion-list *ngIf="entries.onlineEntries.length > 0">
|
<ion-list *ngIf="entries && entries.onlineEntries.length > 0">
|
||||||
<ng-container *ngFor="let entry of entries.onlineEntries; let index = index">
|
<ng-container *ngFor="let entry of entries.onlineEntries; let index = index">
|
||||||
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
|
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -88,11 +88,11 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
<core-empty-box *ngIf="entries.empty && (!isSearch || hasSearched)" icon="fas-list"
|
<core-empty-box *ngIf="(!entries || entries.empty) && (!isSearch || hasSearched)" icon="fas-list"
|
||||||
[message]="'addon.mod_glossary.noentriesfound' | translate">
|
[message]="'addon.mod_glossary.noentriesfound' | translate">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
|
||||||
<core-infinite-loading [enabled]="!entries.completed" [error]="loadMoreError" (action)="loadMoreEntries($event)">
|
<core-infinite-loading [enabled]="entries && !entries.completed" [error]="loadMoreError" (action)="loadMoreEntries($event)">
|
||||||
</core-infinite-loading>
|
</core-infinite-loading>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
|
|
||||||
import { ContextLevel } from '@/core/constants';
|
import { ContextLevel } from '@/core/constants';
|
||||||
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
|
@ -29,16 +30,19 @@ import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import {
|
||||||
|
AddonModGlossaryEntriesSource,
|
||||||
|
AddonModGlossaryEntryItem,
|
||||||
|
AddonModGlossaryFetchMode,
|
||||||
|
} from '../../classes/glossary-entries-source';
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryEntry,
|
AddonModGlossaryEntry,
|
||||||
AddonModGlossaryEntryWithCategory,
|
AddonModGlossaryEntryWithCategory,
|
||||||
AddonModGlossaryGetEntriesOptions,
|
|
||||||
AddonModGlossaryGetEntriesWSResponse,
|
|
||||||
AddonModGlossaryGlossary,
|
AddonModGlossaryGlossary,
|
||||||
AddonModGlossaryProvider,
|
AddonModGlossaryProvider,
|
||||||
} from '../../services/glossary';
|
} from '../../services/glossary';
|
||||||
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
||||||
import {
|
import {
|
||||||
AddonModGlossaryAutoSyncData,
|
AddonModGlossaryAutoSyncData,
|
||||||
AddonModGlossarySyncProvider,
|
AddonModGlossarySyncProvider,
|
||||||
|
@ -63,23 +67,17 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
component = AddonModGlossaryProvider.COMPONENT;
|
component = AddonModGlossaryProvider.COMPONENT;
|
||||||
moduleName = 'glossary';
|
moduleName = 'glossary';
|
||||||
|
|
||||||
isSearch = false;
|
|
||||||
hasSearched = false;
|
|
||||||
canAdd = false;
|
canAdd = false;
|
||||||
loadMoreError = false;
|
loadMoreError = false;
|
||||||
loadingMessage?: string;
|
loadingMessage: string;
|
||||||
entries: AddonModGlossaryEntriesManager;
|
entries!: AddonModGlossaryEntriesManager;
|
||||||
hasOfflineRatings = false;
|
hasOfflineRatings = false;
|
||||||
glossary?: AddonModGlossaryGlossary;
|
|
||||||
|
|
||||||
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
||||||
protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse;
|
|
||||||
protected fetchInvalidate?: () => Promise<void>;
|
|
||||||
protected addEntryObserver?: CoreEventObserver;
|
protected addEntryObserver?: CoreEventObserver;
|
||||||
protected fetchMode?: AddonModGlossaryFetchMode;
|
|
||||||
protected viewMode?: string;
|
|
||||||
protected fetchedEntriesCanLoadMore = false;
|
protected fetchedEntriesCanLoadMore = false;
|
||||||
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
||||||
|
protected sourceUnsubscribe?: () => void;
|
||||||
protected ratingOfflineObserver?: CoreEventObserver;
|
protected ratingOfflineObserver?: CoreEventObserver;
|
||||||
protected ratingSyncObserver?: CoreEventObserver;
|
protected ratingSyncObserver?: CoreEventObserver;
|
||||||
|
|
||||||
|
@ -87,26 +85,47 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
|
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected content?: IonContent,
|
protected content?: IonContent,
|
||||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
@Optional() protected courseContentsPage?: CoreCourseContentsPage,
|
||||||
) {
|
) {
|
||||||
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
||||||
|
|
||||||
this.entries = new AddonModGlossaryEntriesManager(
|
this.loadingMessage = Translate.instant('core.loading');
|
||||||
route.component,
|
}
|
||||||
this,
|
|
||||||
courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : '',
|
get glossary(): AddonModGlossaryGlossary | undefined {
|
||||||
);
|
return this.entries.getSource().glossary;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSearch(): boolean {
|
||||||
|
return this.entries.getSource().isSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasSearched(): boolean {
|
||||||
|
return this.entries.getSource().hasSearched;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
super.ngOnInit();
|
await super.ngOnInit();
|
||||||
|
|
||||||
this.loadingMessage = Translate.instant('core.loading');
|
// Initialize entries manager.
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModGlossaryEntriesSource,
|
||||||
|
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.entries = new AddonModGlossaryEntriesManager(
|
||||||
|
source,
|
||||||
|
this.route.component,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sourceUnsubscribe = source.addListener({
|
||||||
|
onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)),
|
||||||
|
});
|
||||||
|
|
||||||
// When an entry is added, we reload the data.
|
// When an entry is added, we reload the data.
|
||||||
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
|
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
|
||||||
|
@ -143,11 +162,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.entries.start(this.splitView);
|
await this.entries.start(this.splitView);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AddonModGlossary.logView(this.glossary.id, this.viewMode!, this.glossary.name);
|
|
||||||
|
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
|
@ -159,14 +176,18 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.module.id);
|
await this.entries.getSource().loadGlossary();
|
||||||
|
|
||||||
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.description = this.glossary.intro || this.description;
|
this.description = this.glossary.intro || this.description;
|
||||||
this.canAdd = !!this.glossary.canaddentry || false;
|
this.canAdd = !!this.glossary.canaddentry || false;
|
||||||
|
|
||||||
this.dataRetrieved.emit(this.glossary);
|
this.dataRetrieved.emit(this.glossary);
|
||||||
|
|
||||||
if (!this.fetchMode) {
|
if (!this.entries.getSource().fetchMode) {
|
||||||
this.switchMode('letter_all');
|
this.switchMode('letter_all');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +198,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
|
|
||||||
const [hasOfflineRatings] = await Promise.all([
|
const [hasOfflineRatings] = await Promise.all([
|
||||||
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
||||||
this.fetchEntries(),
|
refresh ? this.entries.reload() : this.entries.loadNextPage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.hasOfflineRatings = hasOfflineRatings;
|
this.hasOfflineRatings = hasOfflineRatings;
|
||||||
|
@ -186,59 +207,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to fetch entries.
|
|
||||||
*
|
|
||||||
* @param append True if fetched entries are appended to exsiting ones.
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async fetchEntries(append: boolean = false): Promise<void> {
|
|
||||||
if (!this.fetchFunction) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadMoreError = false;
|
|
||||||
const from = append ? this.entries.onlineEntries.length : 0;
|
|
||||||
|
|
||||||
const result = await this.fetchFunction({
|
|
||||||
from: from,
|
|
||||||
cmId: this.module.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasMoreEntries = from + result.entries.length < result.count;
|
|
||||||
|
|
||||||
if (append) {
|
|
||||||
this.entries.setItems(this.entries.items.concat(result.entries), hasMoreEntries);
|
|
||||||
} else {
|
|
||||||
this.entries.setOnlineEntries(result.entries, hasMoreEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now get the ofline entries.
|
|
||||||
// Check if there are responses stored in offline.
|
|
||||||
const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(this.glossary!.id);
|
|
||||||
|
|
||||||
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
|
|
||||||
this.hasOffline = !!offlineEntries.length;
|
|
||||||
this.entries.setOfflineEntries(offlineEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected async invalidateContent(): Promise<void> {
|
protected async invalidateContent(): Promise<void> {
|
||||||
const promises: Promise<void>[] = [];
|
await this.entries.getSource().invalidateCache();
|
||||||
|
|
||||||
if (this.fetchInvalidate) {
|
|
||||||
promises.push(this.fetchInvalidate());
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.push(AddonModGlossary.invalidateCourseGlossaries(this.courseId));
|
|
||||||
|
|
||||||
if (this.glossary) {
|
|
||||||
promises.push(AddonModGlossary.invalidateCategories(this.glossary.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -277,109 +250,50 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param mode New mode.
|
* @param mode New mode.
|
||||||
*/
|
*/
|
||||||
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
||||||
this.fetchMode = mode;
|
this.entries.getSource().switchMode(mode);
|
||||||
this.isSearch = false;
|
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'author_all':
|
case 'author_all':
|
||||||
// Browse by author.
|
// Browse by author.
|
||||||
this.viewMode = 'author';
|
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByAuthor.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'ALL',
|
|
||||||
'LASTNAME',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByAuthor.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'ALL',
|
|
||||||
'LASTNAME',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.getDivider = (entry) => entry.userfullname;
|
this.getDivider = (entry) => entry.userfullname;
|
||||||
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
|
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cat_all':
|
case 'cat_all': {
|
||||||
// Browse by category.
|
// Browse by category.
|
||||||
this.viewMode = 'cat';
|
const getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByCategory.bind(
|
|
||||||
AddonModGlossary.instance,
|
this.getDivider = getDivider;
|
||||||
this.glossary!.id,
|
this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous);
|
||||||
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByCategory.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
|
||||||
);
|
|
||||||
this.getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
|
|
||||||
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'newest_first':
|
case 'newest_first':
|
||||||
// Newest first.
|
// Newest first.
|
||||||
this.viewMode = 'date';
|
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'CREATION',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'CREATION',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
this.getDivider = undefined;
|
this.getDivider = undefined;
|
||||||
this.showDivider = () => false;
|
this.showDivider = () => false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'recently_updated':
|
case 'recently_updated':
|
||||||
// Recently updated.
|
// Recently updated.
|
||||||
this.viewMode = 'date';
|
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'UPDATE',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'UPDATE',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
this.getDivider = undefined;
|
this.getDivider = undefined;
|
||||||
this.showDivider = () => false;
|
this.showDivider = () => false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'letter_all':
|
case 'letter_all':
|
||||||
default:
|
default: {
|
||||||
// Consider it is 'letter_all'.
|
// Consider it is 'letter_all'.
|
||||||
this.viewMode = 'letter';
|
const getDivider = (entry) => {
|
||||||
this.fetchMode = 'letter_all';
|
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByLetter.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'ALL',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByLetter.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'ALL',
|
|
||||||
);
|
|
||||||
this.getDivider = (entry) => {
|
|
||||||
// Try to get the first letter without HTML tags.
|
// Try to get the first letter without HTML tags.
|
||||||
const noTags = CoreTextUtils.cleanTags(entry.concept);
|
const noTags = CoreTextUtils.cleanTags(entry.concept);
|
||||||
|
|
||||||
return (noTags || entry.concept).substr(0, 1).toUpperCase();
|
return (noTags || entry.concept).substr(0, 1).toUpperCase();
|
||||||
};
|
};
|
||||||
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
|
|
||||||
|
this.getDivider = getDivider;
|
||||||
|
this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,7 +305,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.fetchEntries(true);
|
this.loadMoreError = false;
|
||||||
|
|
||||||
|
await this.entries.loadNextPage();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.loadMoreError = true;
|
this.loadMoreError = true;
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
||||||
|
@ -406,21 +322,34 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param event Event.
|
* @param event Event.
|
||||||
*/
|
*/
|
||||||
async openModePicker(event: MouseEvent): Promise<void> {
|
async openModePicker(event: MouseEvent): Promise<void> {
|
||||||
const mode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousMode = this.entries.getSource().fetchMode;
|
||||||
|
const newMode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
|
||||||
component: AddonModGlossaryModePickerPopoverComponent,
|
component: AddonModGlossaryModePickerPopoverComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
browseModes: this.glossary!.browsemodes,
|
browseModes: this.glossary.browsemodes,
|
||||||
selectedMode: this.isSearch ? '' : this.fetchMode,
|
selectedMode: this.isSearch ? '' : previousMode,
|
||||||
},
|
},
|
||||||
event,
|
event,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mode) {
|
if (!newMode) {
|
||||||
if (mode !== this.fetchMode) {
|
return;
|
||||||
this.changeFetchMode(mode);
|
}
|
||||||
} else if (this.isSearch) {
|
|
||||||
this.toggleSearch();
|
if (newMode !== previousMode) {
|
||||||
}
|
this.changeFetchMode(newMode);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSearch) {
|
||||||
|
this.toggleSearch();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,20 +358,22 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
toggleSearch(): void {
|
toggleSearch(): void {
|
||||||
if (this.isSearch) {
|
if (this.isSearch) {
|
||||||
this.isSearch = false;
|
const fetchMode = this.entries.getSource().fetchMode;
|
||||||
this.hasSearched = false;
|
|
||||||
this.entries.setOnlineEntries(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
|
|
||||||
this.switchMode(this.fetchMode!);
|
|
||||||
} else {
|
|
||||||
// Search for entries. The fetch function will be set when searching.
|
|
||||||
this.getDivider = undefined;
|
|
||||||
this.showDivider = () => false;
|
|
||||||
this.isSearch = true;
|
|
||||||
|
|
||||||
this.fetchedEntries = this.entries.onlineEntries;
|
fetchMode && this.switchMode(fetchMode);
|
||||||
this.fetchedEntriesCanLoadMore = !this.entries.completed;
|
this.entries.getSource().stopSearch(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
|
||||||
this.entries.setItems([], false);
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search for entries. The fetch function will be set when searching.
|
||||||
|
this.fetchedEntries = this.entries.getSource().onlineEntries;
|
||||||
|
this.fetchedEntriesCanLoadMore = !this.entries.completed;
|
||||||
|
this.getDivider = undefined;
|
||||||
|
this.showDivider = () => false;
|
||||||
|
|
||||||
|
this.entries.reset();
|
||||||
|
this.entries.getSource().startSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -451,7 +382,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param mode Mode.
|
* @param mode Mode.
|
||||||
*/
|
*/
|
||||||
changeFetchMode(mode: AddonModGlossaryFetchMode): void {
|
changeFetchMode(mode: AddonModGlossaryFetchMode): void {
|
||||||
this.isSearch = false;
|
|
||||||
this.loadingMessage = Translate.instant('core.loading');
|
this.loadingMessage = Translate.instant('core.loading');
|
||||||
this.content?.scrollToTop();
|
this.content?.scrollToTop();
|
||||||
this.switchMode(mode);
|
this.switchMode(mode);
|
||||||
|
@ -463,7 +393,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* Opens new entry editor.
|
* Opens new entry editor.
|
||||||
*/
|
*/
|
||||||
openNewEntry(): void {
|
openNewEntry(): void {
|
||||||
this.entries.select({ newEntry: true });
|
this.entries.select(AddonModGlossaryEntriesSource.NEW_ENTRY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -473,24 +403,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
search(query: string): void {
|
search(query: string): void {
|
||||||
this.loadingMessage = Translate.instant('core.searching');
|
this.loadingMessage = Translate.instant('core.searching');
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesBySearch.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
query,
|
|
||||||
true,
|
|
||||||
'CONCEPT',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesBySearch.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
query,
|
|
||||||
true,
|
|
||||||
'CONCEPT',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.hasSearched = true;
|
|
||||||
|
this.entries.getSource().search(query);
|
||||||
this.loadContent();
|
this.loadContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,154 +418,44 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
this.addEntryObserver?.off();
|
this.addEntryObserver?.off();
|
||||||
this.ratingOfflineObserver?.off();
|
this.ratingOfflineObserver?.off();
|
||||||
this.ratingSyncObserver?.off();
|
this.ratingSyncObserver?.off();
|
||||||
|
this.sourceUnsubscribe?.call(null);
|
||||||
|
this.entries.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type to select the new entry form.
|
|
||||||
*/
|
|
||||||
type NewEntryForm = { newEntry: true };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of items that can be held by the entries manager.
|
|
||||||
*/
|
|
||||||
type EntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | NewEntryForm;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entries manager.
|
* Entries manager.
|
||||||
*/
|
*/
|
||||||
class AddonModGlossaryEntriesManager extends CorePageItemsListManager<EntryItem> {
|
class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
|
||||||
|
|
||||||
onlineEntries: AddonModGlossaryEntry[] = [];
|
get offlineEntries(): AddonModGlossaryOfflineEntry[] {
|
||||||
offlineEntries: AddonModGlossaryOfflineEntry[] = [];
|
return this.getSource().offlineEntries;
|
||||||
|
|
||||||
protected glossaryPathPrefix: string;
|
|
||||||
protected component: AddonModGlossaryIndexComponent;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
pageComponent: unknown,
|
|
||||||
component: AddonModGlossaryIndexComponent,
|
|
||||||
glossaryPathPrefix: string,
|
|
||||||
) {
|
|
||||||
super(pageComponent);
|
|
||||||
|
|
||||||
this.component = component;
|
|
||||||
this.glossaryPathPrefix = glossaryPathPrefix;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get onlineEntries(): AddonModGlossaryEntry[] {
|
||||||
* Type guard to infer NewEntryForm objects.
|
return this.getSource().onlineEntries;
|
||||||
*
|
|
||||||
* @param entry Item to check.
|
|
||||||
* @return Whether the item is a new entry form.
|
|
||||||
*/
|
|
||||||
isNewEntryForm(entry: EntryItem): entry is NewEntryForm {
|
|
||||||
return 'newEntry' in entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer entry objects.
|
|
||||||
*
|
|
||||||
* @param entry Item to check.
|
|
||||||
* @return Whether the item is an offline entry.
|
|
||||||
*/
|
|
||||||
isOfflineEntry(entry: EntryItem): entry is AddonModGlossaryOfflineEntry {
|
|
||||||
return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer entry objects.
|
|
||||||
*
|
|
||||||
* @param entry Item to check.
|
|
||||||
* @return Whether the item is an offline entry.
|
|
||||||
*/
|
|
||||||
isOnlineEntry(entry: EntryItem): entry is AddonModGlossaryEntry {
|
|
||||||
return 'id' in entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update online entries items.
|
|
||||||
*
|
|
||||||
* @param onlineEntries Online entries.
|
|
||||||
*/
|
|
||||||
setOnlineEntries(onlineEntries: AddonModGlossaryEntry[], hasMoreItems: boolean = false): void {
|
|
||||||
this.setItems((<EntryItem[]> this.offlineEntries).concat(onlineEntries), hasMoreItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update offline entries items.
|
|
||||||
*
|
|
||||||
* @param offlineEntries Offline entries.
|
|
||||||
*/
|
|
||||||
setOfflineEntries(offlineEntries: AddonModGlossaryOfflineEntry[]): void {
|
|
||||||
this.setItems((<EntryItem[]> offlineEntries).concat(this.onlineEntries), this.hasMoreItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
setItems(entries: EntryItem[], hasMoreItems: boolean = false): void {
|
protected getDefaultItem(): AddonModGlossaryEntryItem | null {
|
||||||
super.setItems(entries, hasMoreItems);
|
return this.getSource().onlineEntries[0] || null;
|
||||||
|
|
||||||
this.onlineEntries = [];
|
|
||||||
this.offlineEntries = [];
|
|
||||||
this.items.forEach(entry => {
|
|
||||||
if (this.isOfflineEntry(entry)) {
|
|
||||||
this.offlineEntries.push(entry);
|
|
||||||
} else if (this.isOnlineEntry(entry)) {
|
|
||||||
this.onlineEntries.push(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
resetItems(): void {
|
protected async logActivity(): Promise<void> {
|
||||||
super.resetItems();
|
const glossary = this.getSource().glossary;
|
||||||
this.onlineEntries = [];
|
const viewMode = this.getSource().viewMode;
|
||||||
this.offlineEntries = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (!glossary || !viewMode) {
|
||||||
* @inheritdoc
|
return;
|
||||||
*/
|
|
||||||
protected getItemPath(entry: EntryItem): string {
|
|
||||||
if (this.isOnlineEntry(entry)) {
|
|
||||||
return `${this.glossaryPathPrefix}entry/${entry.id}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isOfflineEntry(entry)) {
|
await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
|
||||||
return `${this.glossaryPathPrefix}edit/${entry.timecreated}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${this.glossaryPathPrefix}edit/0`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
getItemQueryParams(entry: EntryItem): Params {
|
|
||||||
const params: Params = {
|
|
||||||
cmId: this.component.module.id,
|
|
||||||
courseId: this.component.courseId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.isOfflineEntry(entry)) {
|
|
||||||
params.concept = entry.concept;
|
|
||||||
}
|
|
||||||
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getDefaultItem(): EntryItem | null {
|
|
||||||
return this.onlineEntries[0] || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { PopoverController } from '@singletons';
|
import { PopoverController } from '@singletons';
|
||||||
import { AddonModGlossaryFetchMode } from '../index';
|
import { AddonModGlossaryFetchMode } from '../../classes/glossary-entries-source';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to display the mode picker.
|
* Component to display the mode picker.
|
||||||
|
|
|
@ -51,10 +51,12 @@ const mainMenuRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||||
|
data: { swipeEnabled: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||||
|
data: { swipeEnabled: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
||||||
|
@ -65,10 +67,12 @@ const mainMenuRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => CoreScreen.isMobile,
|
() => CoreScreen.isMobile,
|
||||||
|
@ -80,10 +84,12 @@ const courseContentsRoutes: Routes = conditionalRoutes(
|
||||||
{
|
{
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => CoreScreen.isTablet,
|
() => CoreScreen.isTablet,
|
||||||
|
|
|
@ -12,72 +12,75 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-swipe-navigation [manager]="entries">
|
||||||
<form #editFormEl *ngIf="glossary">
|
<core-loading [hideUntil]="loaded">
|
||||||
<ion-item>
|
<form #editFormEl *ngIf="glossary">
|
||||||
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
|
<ion-item>
|
||||||
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept" name="concept">
|
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
|
||||||
</ion-input>
|
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept"
|
||||||
</ion-item>
|
name="concept">
|
||||||
<ion-item>
|
</ion-input>
|
||||||
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
|
</ion-item>
|
||||||
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
|
<ion-item>
|
||||||
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
|
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
|
||||||
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId" elementId="definition_editor"
|
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
|
||||||
[draftExtraParams]="editorExtraParams">
|
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
|
||||||
</core-rich-text-editor>
|
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId"
|
||||||
</ion-item>
|
elementId="definition_editor" [draftExtraParams]="editorExtraParams">
|
||||||
<ion-item *ngIf="categories.length > 0">
|
</core-rich-text-editor>
|
||||||
<ion-label position="stacked" id="addon-mod-glossary-categories-label">
|
</ion-item>
|
||||||
{{ 'addon.mod_glossary.categories' | translate }}
|
<ion-item *ngIf="categories.length > 0">
|
||||||
</ion-label>
|
<ion-label position="stacked" id="addon-mod-glossary-categories-label">
|
||||||
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label"
|
{{ 'addon.mod_glossary.categories' | translate }}
|
||||||
interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
|
</ion-label>
|
||||||
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
|
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label"
|
||||||
<ion-select-option *ngFor="let category of categories" [value]="category.id">
|
interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
|
||||||
{{ category.name }}
|
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
|
||||||
</ion-select-option>
|
<ion-select-option *ngFor="let category of categories" [value]="category.id">
|
||||||
</ion-select>
|
{{ category.name }}
|
||||||
</ion-item>
|
</ion-select-option>
|
||||||
<ion-item>
|
</ion-select>
|
||||||
<ion-label position="stacked" id="addon-mod-glossary-aliases-label">
|
</ion-item>
|
||||||
{{ 'addon.mod_glossary.aliases' | translate }}
|
<ion-item>
|
||||||
</ion-label>
|
<ion-label position="stacked" id="addon-mod-glossary-aliases-label">
|
||||||
<ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases"
|
{{ 'addon.mod_glossary.aliases' | translate }}
|
||||||
aria-labelledby="addon-mod-glossary-aliases-label" name="aliases">
|
</ion-label>
|
||||||
</ion-textarea>
|
<ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases"
|
||||||
</ion-item>
|
aria-labelledby="addon-mod-glossary-aliases-label" name="aliases">
|
||||||
<ion-item-divider>
|
</ion-textarea>
|
||||||
<ion-label>
|
</ion-item>
|
||||||
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item-divider>
|
|
||||||
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
|
|
||||||
[courseId]="courseId">
|
|
||||||
</core-attachments>
|
|
||||||
<ng-container *ngIf="glossary.usedynalink">
|
|
||||||
<ion-item-divider>
|
<ion-item-divider>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
|
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<ion-item class="ion-text-wrap">
|
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
|
||||||
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
|
[courseId]="courseId">
|
||||||
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
|
</core-attachments>
|
||||||
</ion-item>
|
<ng-container *ngIf="glossary.usedynalink">
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item-divider>
|
||||||
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
|
<ion-label>
|
||||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
|
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
|
||||||
</ion-toggle>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item-divider>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
|
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
|
||||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle>
|
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-button class="ion-margin" expand="block" [disabled]="!entry.concept || !entry.definition" (click)="save()">
|
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
|
||||||
{{ 'core.save' | translate }}
|
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
|
||||||
</ion-button>
|
</ion-toggle>
|
||||||
</form>
|
</ion-item>
|
||||||
</core-loading>
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
|
||||||
|
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
<ion-button class="ion-margin" expand="block" [disabled]="!entry.concept || !entry.definition" (click)="save()">
|
||||||
|
{{ 'core.save' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</form>
|
||||||
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -12,9 +12,11 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, ViewChild, ElementRef, Optional } from '@angular/core';
|
import { Component, OnInit, ViewChild, ElementRef, Optional, OnDestroy } from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
|
@ -26,6 +28,8 @@ import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
|
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
||||||
|
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryCategory,
|
AddonModGlossaryCategory,
|
||||||
|
@ -45,7 +49,7 @@ import { AddonModGlossaryOffline } from '../../services/glossary-offline';
|
||||||
selector: 'page-addon-mod-glossary-edit',
|
selector: 'page-addon-mod-glossary-edit',
|
||||||
templateUrl: 'edit.html',
|
templateUrl: 'edit.html',
|
||||||
})
|
})
|
||||||
export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
@ViewChild('editFormEl') formElement?: ElementRef;
|
@ViewChild('editFormEl') formElement?: ElementRef;
|
||||||
|
|
||||||
|
@ -64,6 +68,8 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
timecreated: 0,
|
timecreated: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
entries?: AddonModGlossaryEditEntriesSwipeManager;
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
categories: <string[]> [],
|
categories: <string[]> [],
|
||||||
aliases: '',
|
aliases: '',
|
||||||
|
@ -80,18 +86,30 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
protected originalData?: AddonModGlossaryNewEntryWithFiles;
|
protected originalData?: AddonModGlossaryNewEntryWithFiles;
|
||||||
protected saved = false;
|
protected saved = false;
|
||||||
|
|
||||||
constructor(@Optional() protected splitView: CoreSplitViewComponent) {}
|
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const routeData = this.route.snapshot.data;
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated');
|
this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated');
|
||||||
this.concept = CoreNavigator.getRouteParam<string>('concept') || '';
|
this.concept = CoreNavigator.getRouteParam<string>('concept') || '';
|
||||||
this.editorExtraParams.timecreated = this.timecreated;
|
this.editorExtraParams.timecreated = this.timecreated;
|
||||||
|
|
||||||
|
if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModGlossaryEntriesSource,
|
||||||
|
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.entries = new AddonModGlossaryEditEntriesSwipeManager(source);
|
||||||
|
|
||||||
|
await this.entries.start();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -103,6 +121,13 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.entries?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch required data.
|
* Fetch required data.
|
||||||
*
|
*
|
||||||
|
@ -134,7 +159,11 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadOfflineData(): Promise<void> {
|
protected async loadOfflineData(): Promise<void> {
|
||||||
const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary!.id, this.concept, this.timecreated);
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary.id, this.concept, this.timecreated);
|
||||||
|
|
||||||
this.entry.concept = entry.concept || '';
|
this.entry.concept = entry.concept || '';
|
||||||
this.entry.definition = entry.definition || '';
|
this.entry.definition = entry.definition || '';
|
||||||
|
@ -159,7 +188,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
|
|
||||||
// Treat offline attachments if any.
|
// Treat offline attachments if any.
|
||||||
if (entry.attachments?.offline) {
|
if (entry.attachments?.offline) {
|
||||||
this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary!.id, entry.concept, entry.timecreated);
|
this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary.id, entry.concept, entry.timecreated);
|
||||||
|
|
||||||
this.originalData.files = this.attachments.slice();
|
this.originalData.files = this.attachments.slice();
|
||||||
}
|
}
|
||||||
|
@ -236,6 +265,10 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
definition = CoreTextUtils.formatHtmlLines(definition);
|
definition = CoreTextUtils.formatHtmlLines(definition);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Upload attachments first if any.
|
// Upload attachments first if any.
|
||||||
const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
|
const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
|
||||||
|
|
||||||
|
@ -244,7 +277,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
categories: this.options.categories.join(','),
|
categories: this.options.categories.join(','),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.glossary!.usedynalink) {
|
if (this.glossary.usedynalink) {
|
||||||
options.usedynalink = this.options.usedynalink ? 1 : 0;
|
options.usedynalink = this.options.usedynalink ? 1 : 0;
|
||||||
if (this.options.usedynalink) {
|
if (this.options.usedynalink) {
|
||||||
options.casesensitive = this.options.casesensitive ? 1 : 0;
|
options.casesensitive = this.options.casesensitive ? 1 : 0;
|
||||||
|
@ -253,9 +286,9 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveOffline) {
|
if (saveOffline) {
|
||||||
if (this.entry && !this.glossary!.allowduplicatedentries) {
|
if (this.entry && !this.glossary.allowduplicatedentries) {
|
||||||
// Check if the entry is duplicated in online or offline mode.
|
// Check if the entry is duplicated in online or offline mode.
|
||||||
const isUsed = await AddonModGlossary.isConceptUsed(this.glossary!.id, this.entry.concept, {
|
const isUsed = await AddonModGlossary.isConceptUsed(this.glossary.id, this.entry.concept, {
|
||||||
timeCreated: this.entry.timecreated,
|
timeCreated: this.entry.timecreated,
|
||||||
cmId: this.cmId,
|
cmId: this.cmId,
|
||||||
});
|
});
|
||||||
|
@ -268,7 +301,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
|
|
||||||
// Save entry in offline.
|
// Save entry in offline.
|
||||||
await AddonModGlossaryOffline.addNewEntry(
|
await AddonModGlossaryOffline.addNewEntry(
|
||||||
this.glossary!.id,
|
this.glossary.id,
|
||||||
this.entry.concept,
|
this.entry.concept,
|
||||||
definition,
|
definition,
|
||||||
this.courseId,
|
this.courseId,
|
||||||
|
@ -283,7 +316,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
// Try to send it to server.
|
// Try to send it to server.
|
||||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||||
await AddonModGlossary.addEntry(
|
await AddonModGlossary.addEntry(
|
||||||
this.glossary!.id,
|
this.glossary.id,
|
||||||
this.entry.concept,
|
this.entry.concept,
|
||||||
definition,
|
definition,
|
||||||
this.courseId,
|
this.courseId,
|
||||||
|
@ -293,7 +326,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
timeCreated: timecreated,
|
timeCreated: timecreated,
|
||||||
discardEntry: this.entry,
|
discardEntry: this.entry,
|
||||||
allowOffline: !this.attachments.length,
|
allowOffline: !this.attachments.length,
|
||||||
checkDuplicates: !this.glossary!.allowduplicatedentries,
|
checkDuplicates: !this.glossary.allowduplicatedentries,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -303,12 +336,12 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
|
|
||||||
if (entryId) {
|
if (entryId) {
|
||||||
// Data sent to server, delete stored files (if any).
|
// Data sent to server, delete stored files (if any).
|
||||||
AddonModGlossaryHelper.deleteStoredFiles(this.glossary!.id, this.entry.concept, timecreated);
|
AddonModGlossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated);
|
||||||
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
|
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, {
|
CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, {
|
||||||
glossaryId: this.glossary!.id,
|
glossaryId: this.glossary.id,
|
||||||
entryId: entryId,
|
entryId: entryId,
|
||||||
}, CoreSites.getCurrentSiteId());
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
|
@ -342,7 +375,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
protected async uploadAttachments(
|
protected async uploadAttachments(
|
||||||
timecreated: number,
|
timecreated: number,
|
||||||
): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
|
): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
|
||||||
if (!this.attachments.length) {
|
if (!this.attachments.length || !this.glossary) {
|
||||||
return {
|
return {
|
||||||
saveOffline: false,
|
saveOffline: false,
|
||||||
};
|
};
|
||||||
|
@ -352,7 +385,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
|
const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
|
||||||
this.attachments,
|
this.attachments,
|
||||||
AddonModGlossaryProvider.COMPONENT,
|
AddonModGlossaryProvider.COMPONENT,
|
||||||
this.glossary!.id,
|
this.glossary.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -362,7 +395,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
} catch {
|
} catch {
|
||||||
// Cannot upload them in online, save them in offline.
|
// Cannot upload them in online, save them in offline.
|
||||||
const attachmentsResult = await AddonModGlossaryHelper.storeFiles(
|
const attachmentsResult = await AddonModGlossaryHelper.storeFiles(
|
||||||
this.glossary!.id,
|
this.glossary.id,
|
||||||
this.entry.concept,
|
this.entry.concept,
|
||||||
timecreated,
|
timecreated,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
|
@ -387,3 +420,17 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of glossary entries.
|
||||||
|
*/
|
||||||
|
class AddonModGlossaryEditEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return `${this.getSource().GLOSSARY_PATH_PREFIX}edit/${route.params.timecreated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -12,73 +12,75 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
<core-swipe-navigation [manager]="entries">
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
||||||
</ion-refresher>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<ng-container *ngIf="entry && loaded">
|
<ng-container *ngIf="entry && loaded">
|
||||||
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
||||||
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
|
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>
|
<h2>
|
||||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
|
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
|
||||||
[courseId]="courseId">
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</h2>
|
||||||
|
<p>{{ entry.userfullname }}</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">
|
||||||
|
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"
|
||||||
|
contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</h2>
|
</ion-label>
|
||||||
<p>{{ entry.userfullname }}</p>
|
</ion-item>
|
||||||
</ion-label>
|
<div *ngIf="entry.attachment" lines="none">
|
||||||
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
||||||
</ion-item>
|
</core-file>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
|
</div>
|
||||||
<ion-label>
|
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
|
||||||
<p class="item-heading">
|
<ion-label>
|
||||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId">
|
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||||
</core-format-text>
|
<core-tag-list [tags]="entry.tags"></core-tag-list>
|
||||||
</p>
|
</ion-label>
|
||||||
</ion-label>
|
</ion-item>
|
||||||
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
<ion-item class="ion-text-wrap" *ngIf="!entry.approved">
|
||||||
</ion-item>
|
<ion-label>
|
||||||
<ion-item class="ion-text-wrap">
|
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
|
||||||
<ion-label>
|
</ion-label>
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition" contextLevel="module"
|
</ion-item>
|
||||||
[contextInstanceId]="componentId" [courseId]="courseId">
|
<core-comments *ngIf="glossary && glossary.allowcomments && entry && entry.id > 0 && commentsEnabled" contextLevel="module"
|
||||||
</core-format-text>
|
[instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="entry.id" area="glossary_entry"
|
||||||
</ion-label>
|
[courseId]="glossary.course" [showItem]="true">
|
||||||
</ion-item>
|
</core-comments>
|
||||||
<div *ngIf="entry.attachment" lines="none">
|
<core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||||
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.course"
|
||||||
</core-file>
|
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
|
||||||
</div>
|
</core-rating-rate>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
|
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||||
<ion-label>
|
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course"
|
||||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale">
|
||||||
<core-tag-list [tags]="entry.tags"></core-tag-list>
|
</core-rating-aggregate>
|
||||||
</ion-label>
|
</ng-container>
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="!entry.approved">
|
|
||||||
<ion-label>
|
|
||||||
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<core-comments *ngIf="glossary && glossary.allowcomments && entry && entry.id > 0 && commentsEnabled" contextLevel="module"
|
|
||||||
[instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="entry.id" area="glossary_entry"
|
|
||||||
[courseId]="glossary.course" [showItem]="true">
|
|
||||||
</core-comments>
|
|
||||||
<core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
|
||||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.course"
|
|
||||||
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
|
|
||||||
</core-rating-rate>
|
|
||||||
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
|
||||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course" [aggregateMethod]="glossary.assessed"
|
|
||||||
[scaleId]="glossary.scale">
|
|
||||||
</core-rating-aggregate>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ion-card *ngIf="!entry" class="core-warning-card">
|
<ion-card *ngIf="!entry" class="core-warning-card">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label>
|
<ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -12,7 +12,9 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
||||||
import { CoreComments } from '@features/comments/services/comments';
|
import { CoreComments } from '@features/comments/services/comments';
|
||||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||||
|
@ -21,6 +23,8 @@ import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
||||||
|
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryEntry,
|
AddonModGlossaryEntry,
|
||||||
|
@ -35,13 +39,14 @@ import {
|
||||||
selector: 'page-addon-mod-glossary-entry',
|
selector: 'page-addon-mod-glossary-entry',
|
||||||
templateUrl: 'entry.html',
|
templateUrl: 'entry.html',
|
||||||
})
|
})
|
||||||
export class AddonModGlossaryEntryPage implements OnInit {
|
export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
|
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
|
||||||
|
|
||||||
component = AddonModGlossaryProvider.COMPONENT;
|
component = AddonModGlossaryProvider.COMPONENT;
|
||||||
componentId?: number;
|
componentId?: number;
|
||||||
entry?: AddonModGlossaryEntry;
|
entry?: AddonModGlossaryEntry;
|
||||||
|
entries?: AddonModGlossaryEntryEntriesSwipeManager;
|
||||||
glossary?: AddonModGlossaryGlossary;
|
glossary?: AddonModGlossaryGlossary;
|
||||||
loaded = false;
|
loaded = false;
|
||||||
showAuthor = false;
|
showAuthor = false;
|
||||||
|
@ -53,15 +58,30 @@ export class AddonModGlossaryEntryPage implements OnInit {
|
||||||
|
|
||||||
protected entryId!: number;
|
protected entryId!: number;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const routeData = this.route.snapshot.data;
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId');
|
this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId');
|
||||||
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
||||||
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
||||||
|
|
||||||
|
if (routeData.swipeEnabled ?? true) {
|
||||||
|
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModGlossaryEntriesSource,
|
||||||
|
[this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
|
||||||
|
|
||||||
|
await this.entries.start();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -73,16 +93,23 @@ export class AddonModGlossaryEntryPage implements OnInit {
|
||||||
try {
|
try {
|
||||||
await this.fetchEntry();
|
await this.fetchEntry();
|
||||||
|
|
||||||
if (!this.glossary) {
|
if (!this.glossary || !this.componentId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId!, this.glossary.name));
|
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId, this.glossary.name));
|
||||||
} finally {
|
} finally {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.entries?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh the data.
|
* Refresh the data.
|
||||||
*
|
*
|
||||||
|
@ -152,3 +179,17 @@ export class AddonModGlossaryEntryPage implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of glossary entries.
|
||||||
|
*/
|
||||||
|
class AddonModGlossaryEntryEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${route.params.entryId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -37,9 +37,9 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
return args.map(argument => String(argument)).join('-');
|
return args.map(argument => String(argument)).join('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
private items: Item[] | null = null;
|
protected items: Item[] | null = null;
|
||||||
private hasMoreItems = true;
|
protected hasMoreItems = true;
|
||||||
private listeners: CoreItemsListSourceListener<Item>[] = [];
|
protected listeners: CoreItemsListSourceListener<Item>[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether any page has been loaded.
|
* Check whether any page has been loaded.
|
||||||
|
|
Loading…
Reference in New Issue