MOBILE-3644 glossary: Migrate index page
parent
010475b790
commit
184a7b561b
|
@ -0,0 +1,43 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { AddonModGlossaryIndexComponent } from './index/index';
|
||||
import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-picker';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModGlossaryIndexComponent,
|
||||
AddonModGlossaryModePickerPopoverComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreCourseComponentsModule,
|
||||
CoreSearchComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModGlossaryIndexComponent,
|
||||
AddonModGlossaryModePickerPopoverComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModGlossaryIndexComponent,
|
||||
AddonModGlossaryModePickerPopoverComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModGlossaryComponentsModule {}
|
|
@ -0,0 +1,107 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons slot="end">
|
||||
<ion-button *ngIf="glossary && glossary.browsemodes && glossary.browsemodes.length > 1" (click)="openModePicker($event)"
|
||||
[attr.aria-label]="'addon.mod_glossary.browsemode' | translate">
|
||||
<ion-icon name="fas-sort"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<ion-button *ngIf="glossary" (click)="toggleSearch()" [attr.aria-label]="'addon.mod_glossary.bysearch' | translate">
|
||||
<ion-icon name="fas-search"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
|
||||
[href]="externalUrl" iconAction="fas-external-link-alt">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
|
||||
(action)="expandDescription()" iconAction="fas-arrow-right">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
|
||||
iconAction="far-newspaper" (action)="gotoBlog()">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700"
|
||||
[content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon"
|
||||
[closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600"
|
||||
(action)="doRefresh(null, $event, true)" [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon"
|
||||
[closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="canAdd" [priority]="550" [content]="'addon.mod_glossary.addentry' | translate"
|
||||
(action)="openNewEntry()" iconAction="fas-plus">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
|
||||
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
|
||||
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
<!-- Content. -->
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-search-box *ngIf="isSearch" (onSubmit)="search($event)" [placeholder]="'addon.mod_glossary.searchquery' | translate"
|
||||
[autoFocus]="true" [lengthCheck]="2" (onClear)="toggleSearch()" searchArea="AddonModGlossary-{{module.id}}">
|
||||
</core-search-box>
|
||||
|
||||
<core-loading [hideUntil]="entries.loaded" class="core-loading-center">
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
||||
<!-- Has offline data to be synchronized -->
|
||||
<ion-card class="core-warning-card" *ngIf="hasOffline || hasOfflineRatings">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-list *ngIf="!isSearch && entries.offlineEntries.length > 0">
|
||||
<ion-item-divider>
|
||||
<ion-label>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let entry of entries.offlineEntries" (click)="entries.select(entry)" detail="false"
|
||||
[class.core-selected-item]="entries.isSelected(entry)">
|
||||
<ion-label>
|
||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="glossary!.coursemodule"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list *ngIf="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])">
|
||||
{{ getDivider!(entry) }}
|
||||
</ion-item-divider>
|
||||
|
||||
<ion-item (click)="entries.select(entry)" [class.core-selected-item]="entries.isSelected(entry)" detail="false">
|
||||
<ion-label>
|
||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="glossary!.coursemodule"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
<core-empty-box *ngIf="entries.empty" 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>
|
||||
</core-loading>
|
||||
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd">
|
||||
<ion-fab-button (click)="openNewEntry()" [attr.aria-label]="'addon.mod_glossary.addentry' | translate">
|
||||
<ion-icon name="fas-plus"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</core-split-view>
|
|
@ -0,0 +1,646 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ContextLevel } from '@/core/constants';
|
||||
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { CorePageItemsListManager } from '@classes/page-items-list-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';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreRatingProvider } from '@features/rating/services/rating';
|
||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { PopoverController, Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import {
|
||||
AddonModGlossary,
|
||||
AddonModGlossaryEntry,
|
||||
AddonModGlossaryEntryWithCategory,
|
||||
AddonModGlossaryGetEntriesOptions,
|
||||
AddonModGlossaryGetEntriesWSResponse,
|
||||
AddonModGlossaryGlossary,
|
||||
AddonModGlossaryProvider,
|
||||
} from '../../services/glossary';
|
||||
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
||||
import {
|
||||
AddonModGlossaryAutoSyncData,
|
||||
AddonModGlossarySyncProvider,
|
||||
AddonModGlossarySyncResult,
|
||||
} from '../../services/glossary-sync';
|
||||
import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch';
|
||||
import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker';
|
||||
|
||||
/**
|
||||
* Component that displays a glossary entry page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-glossary-index',
|
||||
templateUrl: 'addon-mod-glossary-index.html',
|
||||
})
|
||||
export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent
|
||||
implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||
|
||||
component = AddonModGlossaryProvider.COMPONENT;
|
||||
moduleName = 'glossary';
|
||||
|
||||
isSearch = false;
|
||||
canAdd = false;
|
||||
loadMoreError = false;
|
||||
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 ratingOfflineObserver?: CoreEventObserver;
|
||||
protected ratingSyncObserver?: CoreEventObserver;
|
||||
|
||||
getDivider?: (entry: AddonModGlossaryEntry) => string;
|
||||
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
|
||||
|
||||
constructor(
|
||||
route: ActivatedRoute,
|
||||
protected content?: IonContent,
|
||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||
) {
|
||||
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
||||
|
||||
this.entries = new AddonModGlossaryEntriesManager(
|
||||
route.component,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
super.ngOnInit();
|
||||
|
||||
this.loadingMessage = Translate.instant('core.loading');
|
||||
|
||||
// When an entry is added, we reload the data.
|
||||
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
|
||||
if (this.glossary && this.glossary.id === data.glossaryId) {
|
||||
this.showLoadingAndRefresh(false);
|
||||
|
||||
// Check completion since it could be configured to complete once the user adds a new entry.
|
||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for offline ratings saved and synced.
|
||||
this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
|
||||
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
||||
&& data.instanceId == this.glossary.coursemodule) {
|
||||
this.hasOfflineRatings = true;
|
||||
}
|
||||
});
|
||||
this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
|
||||
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
||||
&& data.instanceId == this.glossary.coursemodule) {
|
||||
this.hasOfflineRatings = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
await this.loadContent(false, true);
|
||||
|
||||
if (!this.glossary) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
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);
|
||||
|
||||
this.description = this.glossary.intro || this.description;
|
||||
this.canAdd = (AddonModGlossary.isPluginEnabledForEditing() && !!this.glossary.canaddentry) || false;
|
||||
|
||||
this.dataRetrieved.emit(this.glossary);
|
||||
|
||||
if (!this.fetchMode) {
|
||||
this.switchMode('letter_all');
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
// Try to synchronize the glossary.
|
||||
await this.syncActivity(showErrors);
|
||||
}
|
||||
|
||||
const [hasOfflineRatings] = await Promise.all([
|
||||
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
||||
this.fetchEntries(),
|
||||
]);
|
||||
|
||||
this.hasOfflineRatings = hasOfflineRatings;
|
||||
} finally {
|
||||
this.fillContextMenu(refresh);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected sync(): Promise<AddonModGlossarySyncResult> {
|
||||
return AddonModGlossaryPrefetchHandler.sync(this.module, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @return Whether it succeed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModGlossarySyncResult): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares sync event data with current data to check if refresh content is needed.
|
||||
*
|
||||
* @param syncEventData Data receiven on sync observer.
|
||||
* @return True if refresh is needed, false otherwise.
|
||||
*/
|
||||
protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncData): boolean {
|
||||
return !!this.glossary && syncEventData.glossaryId == this.glossary.id &&
|
||||
syncEventData.userId == CoreSites.getCurrentSiteUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change fetch mode.
|
||||
*
|
||||
* @param mode New mode.
|
||||
*/
|
||||
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
||||
this.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',
|
||||
);
|
||||
this.getDivider = (entry) => entry.userfullname;
|
||||
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
|
||||
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,
|
||||
);
|
||||
this.getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
|
||||
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.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:
|
||||
// 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) => {
|
||||
// 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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to load more entries.
|
||||
*
|
||||
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
||||
try {
|
||||
await this.fetchEntries(true);
|
||||
} catch (error) {
|
||||
this.loadMoreError = true;
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
||||
} finally {
|
||||
infiniteComplete && infiniteComplete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the mode picker menu.
|
||||
*
|
||||
* @param event Event.
|
||||
*/
|
||||
async openModePicker(event: MouseEvent): Promise<void> {
|
||||
const popover = await PopoverController.create({
|
||||
component: AddonModGlossaryModePickerPopoverComponent,
|
||||
componentProps: {
|
||||
browseModes: this.glossary!.browsemodes,
|
||||
selectedMode: this.isSearch ? '' : this.fetchMode,
|
||||
},
|
||||
event,
|
||||
});
|
||||
|
||||
popover.present();
|
||||
|
||||
const result = await popover.onDidDismiss<AddonModGlossaryFetchMode>();
|
||||
|
||||
const mode = result.data;
|
||||
if (mode) {
|
||||
if (mode !== this.fetchMode) {
|
||||
this.changeFetchMode(mode);
|
||||
} else if (this.isSearch) {
|
||||
this.toggleSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles between search and fetch mode.
|
||||
*/
|
||||
toggleSearch(): void {
|
||||
if (this.isSearch) {
|
||||
this.isSearch = 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;
|
||||
this.fetchedEntriesCanLoadMore = !this.entries.completed;
|
||||
this.entries.setItems([], false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change fetch mode.
|
||||
*
|
||||
* @param mode Mode.
|
||||
*/
|
||||
changeFetchMode(mode: AddonModGlossaryFetchMode): void {
|
||||
this.isSearch = false;
|
||||
this.loadingMessage = Translate.instant('core.loading');
|
||||
this.content?.scrollToTop();
|
||||
this.switchMode(mode);
|
||||
this.loaded = false;
|
||||
this.loadContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens new entry editor.
|
||||
*/
|
||||
openNewEntry(): void {
|
||||
this.entries.select({ newEntry: true });
|
||||
// @todo
|
||||
// const params = {
|
||||
// courseId: this.courseId,
|
||||
// module: this.module,
|
||||
// glossary: this.glossary,
|
||||
// entry: entry,
|
||||
// };
|
||||
// this.splitviewCtrl.getMasterNav().push('AddonModGlossaryEditPage', params);
|
||||
// this.selectedEntry = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search entries.
|
||||
*
|
||||
* @param query Text entered on the search box.
|
||||
*/
|
||||
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.loadContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
|
||||
this.addEntryObserver?.off();
|
||||
this.ratingOfflineObserver?.off();
|
||||
this.ratingSyncObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
|
||||
onlineEntries: AddonModGlossaryEntry[] = [];
|
||||
offlineEntries: AddonModGlossaryOfflineEntry[] = [];
|
||||
|
||||
constructor(pageComponent: unknown) {
|
||||
super(pageComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getItemQueryParams(entry: EntryItem): Params {
|
||||
// @todo
|
||||
return {
|
||||
// courseId: this.component.courseId,
|
||||
// cmId: this.component.module.id,
|
||||
// forumId: this.component.forum!.id,
|
||||
// ...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
this.onlineEntries.concat(onlineEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update offline entries items.
|
||||
*
|
||||
* @param offlineEntries Offline entries.
|
||||
*/
|
||||
setOfflineEntries(offlineEntries: AddonModGlossaryOfflineEntry[]): void {
|
||||
this.setItems((<EntryItem[]> offlineEntries).concat(this.onlineEntries), this.hasMoreItems);
|
||||
this.offlineEntries = offlineEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
resetItems(): void {
|
||||
super.resetItems();
|
||||
this.onlineEntries = [];
|
||||
this.offlineEntries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemPath(entry: EntryItem): string {
|
||||
if (this.isOnlineEntry(entry)) {
|
||||
return `entry/${entry.id}`;
|
||||
}
|
||||
|
||||
if (this.isOfflineEntry(entry)) {
|
||||
return `edit/${entry.timecreated}`;
|
||||
}
|
||||
|
||||
return 'edit/0';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';
|
|
@ -0,0 +1,6 @@
|
|||
<ion-radio-group [(ngModel)]="selectedMode" (ionChange)="modePicked()">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let mode of modes">
|
||||
<ion-label>{{ mode.langkey | translate }}</ion-label>
|
||||
<ion-radio slot="end" [value]="mode.key"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
|
@ -0,0 +1,64 @@
|
|||
// (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 { Component, Input, OnInit } from '@angular/core';
|
||||
import { PopoverController } from '@singletons';
|
||||
import { AddonModGlossaryFetchMode } from '../index';
|
||||
|
||||
/**
|
||||
* Component to display the mode picker.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-glossary-mode-picker-popover',
|
||||
templateUrl: 'addon-mod-glossary-mode-picker.html',
|
||||
})
|
||||
export class AddonModGlossaryModePickerPopoverComponent implements OnInit {
|
||||
|
||||
@Input() browseModes: string[] = [];
|
||||
@Input() selectedMode = '';
|
||||
|
||||
modes: { key: AddonModGlossaryFetchMode; langkey: string }[] = [];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.browseModes.forEach((mode) => {
|
||||
switch (mode) {
|
||||
case 'letter' :
|
||||
this.modes.push({ key: 'letter_all', langkey: 'addon.mod_glossary.byalphabet' });
|
||||
break;
|
||||
case 'cat' :
|
||||
this.modes.push({ key: 'cat_all', langkey: 'addon.mod_glossary.bycategory' });
|
||||
break;
|
||||
case 'date' :
|
||||
this.modes.push({ key: 'newest_first', langkey: 'addon.mod_glossary.bynewestfirst' });
|
||||
this.modes.push({ key: 'recently_updated', langkey: 'addon.mod_glossary.byrecentlyupdated' });
|
||||
break;
|
||||
case 'author' :
|
||||
this.modes.push({ key: 'author_all', langkey: 'addon.mod_glossary.byauthor' });
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when a mode is clicked.
|
||||
*/
|
||||
modePicked(): void {
|
||||
PopoverController.dismiss(this.selectedMode);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { AddonModGlossaryComponentsModule } from './components/components.module';
|
||||
import { AddonModGlossaryIndexPage } from './pages/index/index';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: ':courseId/:cmId',
|
||||
component: AddonModGlossaryIndexPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
AddonModGlossaryComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonModGlossaryIndexPage,
|
||||
],
|
||||
})
|
||||
export class AddonModGlossaryLazyModule {}
|
|
@ -13,12 +13,15 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
|
||||
import { CoreCronDelegate } from '@services/cron';
|
||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||
import { AddonModGlossaryComponentsModule } from './components/components.module';
|
||||
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/glossary';
|
||||
import { AddonModGlossaryProvider } from './services/glossary';
|
||||
import { AddonModGlossaryHelperProvider } from './services/glossary-helper';
|
||||
|
@ -28,7 +31,7 @@ import { AddonModGlossaryEditLinkHandler } from './services/handlers/edit-link';
|
|||
import { AddonModGlossaryEntryLinkHandler } from './services/handlers/entry-link';
|
||||
import { AddonModGlossaryIndexLinkHandler } from './services/handlers/index-link';
|
||||
import { AddonModGlossaryListLinkHandler } from './services/handlers/list-link';
|
||||
import { AddonModGlossaryModuleHandler } from './services/handlers/module';
|
||||
import { AddonModGlossaryModuleHandler, AddonModGlossaryModuleHandlerService } from './services/handlers/module';
|
||||
import { AddonModGlossaryPrefetchHandler } from './services/handlers/prefetch';
|
||||
import { AddonModGlossarySyncCronHandler } from './services/handlers/sync-cron';
|
||||
import { AddonModGlossaryTagAreaHandler } from './services/handlers/tag-area';
|
||||
|
@ -40,8 +43,17 @@ export const ADDON_MOD_GLOSSARY_SERVICES: Type<unknown>[] = [
|
|||
AddonModGlossaryHelperProvider,
|
||||
];
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
||||
loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
AddonModGlossaryComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<addon-mod-glossary-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)">
|
||||
</addon-mod-glossary-index>
|
||||
</ion-content>
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { Component, ViewChild } from '@angular/core';
|
||||
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
|
||||
import { AddonModGlossaryIndexComponent } from '../../components/index';
|
||||
|
||||
/**
|
||||
* Page that displays a glossary.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-glossary-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModGlossaryIndexPage extends CoreCourseModuleMainActivityPage<AddonModGlossaryIndexComponent> {
|
||||
|
||||
@ViewChild(AddonModGlossaryIndexComponent) activityComponent?: AddonModGlossaryIndexComponent;
|
||||
|
||||
}
|
Loading…
Reference in New Issue