commit
9258bc0a1e
|
@ -24,9 +24,7 @@ import { makeSingleton, Translate } from '@singletons';
|
||||||
/**
|
/**
|
||||||
* Service that provides some helper functions regarding private and site files.
|
* Service that provides some helper functions regarding private and site files.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class AddonPrivateFilesHelperProvider {
|
export class AddonPrivateFilesHelperProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -25,9 +25,7 @@ const ROOT_CACHE_KEY = 'mmaFiles:';
|
||||||
/**
|
/**
|
||||||
* Service to handle my files and site files.
|
* Service to handle my files and site files.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class AddonPrivateFilesProvider {
|
export class AddonPrivateFilesProvider {
|
||||||
|
|
||||||
// Keep old names for backwards compatibility.
|
// Keep old names for backwards compatibility.
|
||||||
|
|
|
@ -37,12 +37,14 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
hideMenu = true; // It will be unhidden when items are added.
|
hideMenu = true; // It will be unhidden when items are added.
|
||||||
expanded = false;
|
expanded = false;
|
||||||
|
uniqueId: string;
|
||||||
|
|
||||||
protected items: CoreContextMenuItemComponent[] = [];
|
protected items: CoreContextMenuItemComponent[] = [];
|
||||||
protected itemsMovedToParent: CoreContextMenuItemComponent[] = [];
|
protected itemsMovedToParent: CoreContextMenuItemComponent[] = [];
|
||||||
protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
|
protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
|
||||||
protected instanceId: string;
|
protected instanceId: string;
|
||||||
protected parentContextMenu?: CoreContextMenuComponent;
|
protected parentContextMenu?: CoreContextMenuComponent;
|
||||||
protected uniqueId: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected popoverCtrl: PopoverController,
|
protected popoverCtrl: PopoverController,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ion-infinite-scroll [disabled]="!enabled || error || loadingMore" (ionInfinite)="loadMore($event)" [position]="position">
|
<ion-infinite-scroll [disabled]="!enabled || error || loadingMore" (ionInfinite)="loadMore()" [position]="position">
|
||||||
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||||
</ion-infinite-scroll>
|
</ion-infinite-scroll>
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { CoreUtils, CoreUtilsProvider } from '@services/utils/utils';
|
||||||
import { Platform } from '@singletons';
|
import { Platform } from '@singletons';
|
||||||
|
|
||||||
import { mock, mockSingleton, RenderConfig, renderWrapperComponent } from '@/testing/utils';
|
import { mock, mockSingleton, RenderConfig, renderWrapperComponent } from '@/testing/utils';
|
||||||
|
import { CoreFilter } from '@features/filter/services/filter';
|
||||||
|
|
||||||
describe('CoreFormatTextDirective', () => {
|
describe('CoreFormatTextDirective', () => {
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ describe('CoreFormatTextDirective', () => {
|
||||||
const sentence = Faker.lorem.sentence();
|
const sentence = Faker.lorem.sentence();
|
||||||
|
|
||||||
mockSingleton(CoreSites, { getSite: () => Promise.reject() });
|
mockSingleton(CoreSites, { getSite: () => Promise.reject() });
|
||||||
|
mockSingleton(CoreFilter, { formatText: (text) => Promise.resolve(text) });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const fixture = await renderWrapperComponent(
|
const fixture = await renderWrapperComponent(
|
||||||
|
@ -85,6 +87,7 @@ describe('CoreFormatTextDirective', () => {
|
||||||
getSite: jest.fn(() => Promise.resolve(site)),
|
getSite: jest.fn(() => Promise.resolve(site)),
|
||||||
getCurrentSite: () => Promise.resolve(site),
|
getCurrentSite: () => Promise.resolve(site),
|
||||||
});
|
});
|
||||||
|
mockSingleton(CoreFilter, { formatText: (text) => Promise.resolve(text) });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const fixture = await renderWrapperComponent(
|
const fixture = await renderWrapperComponent(
|
||||||
|
|
|
@ -27,9 +27,7 @@ import { Params } from '@angular/router';
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding content links.
|
* Service that provides some features regarding content links.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreContentLinksHelperProvider {
|
export class CoreContentLinksHelperProvider {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
@ -107,9 +107,7 @@ export type CorePrefetchStatusInfo = {
|
||||||
/**
|
/**
|
||||||
* Helper to gather some common course functions.
|
* Helper to gather some common course functions.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreCourseHelperProvider {
|
export class CoreCourseHelperProvider {
|
||||||
|
|
||||||
protected courseDwnPromises: { [s: string]: { [id: number]: Promise<void> } } = {};
|
protected courseDwnPromises: { [s: string]: { [id: number]: Promise<void> } } = {};
|
||||||
|
|
|
@ -21,9 +21,7 @@ import { CoreStatusWithWarningsWSResponse } from '@services/ws';
|
||||||
/**
|
/**
|
||||||
* Service to handle offline data for courses.
|
* Service to handle offline data for courses.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreCourseOfflineProvider {
|
export class CoreCourseOfflineProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -41,9 +41,7 @@ const ROOT_CACHE_KEY = 'mmCourse:';
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding a course.
|
* Service that provides some features regarding a course.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreCourseProvider {
|
export class CoreCourseProvider {
|
||||||
|
|
||||||
static readonly ALL_SECTIONS_ID = -2;
|
static readonly ALL_SECTIONS_ID = -2;
|
||||||
|
|
|
@ -98,10 +98,10 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
|
||||||
/* if (this.isEnrolled) {
|
/* if (this.isEnrolled) {
|
||||||
CoreCourseHelper.instance.openCourse(this.course);
|
CoreCourseHelper.instance.openCourse(this.course);
|
||||||
} else {
|
} else {
|
||||||
this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } });
|
this.navCtrl.navigateForward('/main/home/courses/preview', { queryParams: { course: this.course } });
|
||||||
} */
|
} */
|
||||||
// @todo while opencourse function is not completed, open preview page.
|
// @todo while opencourse function is not completed, open preview page.
|
||||||
this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } });
|
this.navCtrl.navigateForward('/main/home/courses/preview', { queryParams: { course: this.course } });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<section *ngFor="let category of categories">
|
<section *ngFor="let category of categories">
|
||||||
<ion-item class="ion-text-wrap" router-direction="forward" [routerLink]="['/courses/categories', category.id]"
|
<ion-item class="ion-text-wrap" router-direction="forward" [routerLink]="['/main/home/courses/categories', category.id]"
|
||||||
[title]="category.name" detail>
|
[title]="category.name" detail>
|
||||||
<ion-icon name="fas-folder" slot="start"></ion-icon>
|
<ion-icon name="fas-folder" slot="start"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<img [src]="courseImageUrl" core-external-content alt=""/>
|
<img [src]="courseImageUrl" core-external-content alt=""/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="core-course-thumb-parallax-content">
|
<div class="core-course-thumb-parallax-content" *ngIf="course">
|
||||||
<ion-item class="ion-text-wrap" (click)="openCourse()" [title]="course.fullname" [attr.details]="!avoidOpenCourse && canAccessCourse">
|
<ion-item class="ion-text-wrap" (click)="openCourse()" [title]="course.fullname" [attr.details]="!avoidOpenCourse && canAccessCourse">
|
||||||
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
|
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
<h2>{{ 'core.teachers' | translate }}</h2>
|
<h2>{{ 'core.teachers' | translate }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link
|
<!-- @todo <ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link
|
||||||
[userId]="contact.id"
|
[userId]="contact.id"
|
||||||
[courseId]="isEnrolled ? course.id : null"
|
[courseId]="isEnrolled ? course.id : null"
|
||||||
[attr.aria-label]="'core.viewprofile' | translate">
|
[attr.aria-label]="'core.viewprofile' | translate">
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{contact.fullname}}</h2>
|
<h2>{{contact.fullname}}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>-->
|
||||||
<ion-item-divider></ion-item-divider>
|
<ion-item-divider></ion-item-divider>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -102,10 +102,10 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false"
|
<ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false"
|
||||||
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate">
|
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate">
|
||||||
<ion-icon *ngIf="!prefetchCourseData.status != statusDownloaded && !prefetchCourseData.loading"
|
<ion-icon *ngIf="(prefetchCourseData.status != statusDownloaded) && !prefetchCourseData.loading"
|
||||||
[name]="prefetchCourseData.icon" slot="start">
|
[name]="prefetchCourseData.icon" slot="start">
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
<ion-icon *ngIf="prefetchCourseData.status == statusDownloaded && !prefetchCourseData.loading"
|
<ion-icon *ngIf="(prefetchCourseData.status == statusDownloaded) && !prefetchCourseData.loading"
|
||||||
slot="start" [name]="prefetchCourseData.icon" color="success"
|
slot="start" [name]="prefetchCourseData.icon" color="success"
|
||||||
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate" role="status">
|
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate" role="status">
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
|
|
|
@ -64,6 +64,7 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy {
|
||||||
downloadCourseEnabled: boolean;
|
downloadCourseEnabled: boolean;
|
||||||
courseUrl = '';
|
courseUrl = '';
|
||||||
courseImageUrl?: string;
|
courseImageUrl?: string;
|
||||||
|
isMobile: boolean;
|
||||||
|
|
||||||
protected isGuestEnabled = false;
|
protected isGuestEnabled = false;
|
||||||
protected guestInstanceId?: number;
|
protected guestInstanceId?: number;
|
||||||
|
@ -71,7 +72,6 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy {
|
||||||
protected waitStart = 0;
|
protected waitStart = 0;
|
||||||
protected enrolUrl = '';
|
protected enrolUrl = '';
|
||||||
protected paypalReturnUrl = '';
|
protected paypalReturnUrl = '';
|
||||||
protected isMobile: boolean;
|
|
||||||
protected pageDestroyed = false;
|
protected pageDestroyed = false;
|
||||||
protected courseStatusObserver?: CoreEventObserver;
|
protected courseStatusObserver?: CoreEventObserver;
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy {
|
||||||
* Open page to manage courses storage.
|
* Open page to manage courses storage.
|
||||||
*/
|
*/
|
||||||
manageCoursesStorage(): void {
|
manageCoursesStorage(): void {
|
||||||
// @todo this.navCtrl.navigateForward(['/courses/storage']);
|
// @todo this.navCtrl.navigateForward(['/main/home/courses/storage']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)"
|
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch()"
|
||||||
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" autoFocus="true"
|
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" autoFocus="true"
|
||||||
searchArea="CoreCoursesSearch"></core-search-box>
|
searchArea="CoreCoursesSearch"></core-search-box>
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,7 @@ import { CoreWSExternalFile } from '@services/ws';
|
||||||
/**
|
/**
|
||||||
* Helper to gather some common courses functions.
|
* Helper to gather some common courses functions.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreCoursesHelperProvider {
|
export class CoreCoursesHelperProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -26,9 +26,7 @@ const ROOT_CACHE_KEY = 'mmCourses:';
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding lists of courses and categories.
|
* Service that provides some features regarding lists of courses and categories.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreCoursesProvider {
|
export class CoreCoursesProvider {
|
||||||
|
|
||||||
static readonly SEARCH_PER_PAGE = 20;
|
static readonly SEARCH_PER_PAGE = 20;
|
||||||
|
|
|
@ -23,9 +23,7 @@ import { CaptureMediaComponentInputs, CoreEmulatorCaptureMediaComponent } from '
|
||||||
/**
|
/**
|
||||||
* Helper service with some features to capture media (image, audio, video).
|
* Helper service with some features to capture media (image, audio, video).
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreEmulatorCaptureHelperProvider {
|
export class CoreEmulatorCaptureHelperProvider {
|
||||||
|
|
||||||
protected possibleAudioMimeTypes = {
|
protected possibleAudioMimeTypes = {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { CoreLoginModule } from './login/login.module';
|
||||||
import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
|
import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
|
||||||
import { CoreSettingsModule } from './settings/settings.module';
|
import { CoreSettingsModule } from './settings/settings.module';
|
||||||
import { CoreSiteHomeModule } from './sitehome/sitehome.module';
|
import { CoreSiteHomeModule } from './sitehome/sitehome.module';
|
||||||
|
import { CoreTagModule } from './tag/tag.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -33,6 +34,7 @@ import { CoreSiteHomeModule } from './sitehome/sitehome.module';
|
||||||
CoreMainMenuModule,
|
CoreMainMenuModule,
|
||||||
CoreSettingsModule,
|
CoreSettingsModule,
|
||||||
CoreSiteHomeModule,
|
CoreSiteHomeModule,
|
||||||
|
CoreTagModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreFeaturesModule {}
|
export class CoreFeaturesModule {}
|
||||||
|
|
|
@ -139,9 +139,7 @@ export interface CoreFileUploaderHandlerDataToReturn extends CoreFileUploaderHan
|
||||||
/**
|
/**
|
||||||
* Delegate to register handlers to be shown in the file picker.
|
* Delegate to register handlers to be shown in the file picker.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreFileUploaderDelegateService extends CoreDelegate<CoreFileUploaderHandler> {
|
export class CoreFileUploaderDelegateService extends CoreDelegate<CoreFileUploaderHandler> {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -38,9 +38,7 @@ import { CoreWSUploadFileResult } from '@services/ws';
|
||||||
/**
|
/**
|
||||||
* Helper service to upload files.
|
* Helper service to upload files.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreFileUploaderHelperProvider {
|
export class CoreFileUploaderHelperProvider {
|
||||||
|
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
|
|
|
@ -45,9 +45,7 @@ export interface CoreFileUploaderOptions extends CoreWSFileUploadOptions {
|
||||||
/**
|
/**
|
||||||
* Service to upload files.
|
* Service to upload files.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreFileUploaderProvider {
|
export class CoreFileUploaderProvider {
|
||||||
|
|
||||||
static readonly LIMITED_SIZE_WARNING = 1048576; // 1 MB.
|
static readonly LIMITED_SIZE_WARNING = 1048576; // 1 MB.
|
||||||
|
|
|
@ -92,4 +92,4 @@ export class CoreFileUploaderAudioHandlerService implements CoreFileUploaderHand
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CoreFileUploaderAudioHandler extends makeSingleton(CoreFileUploaderAudioHandlerService) {}
|
export class CoreFileUploaderAudioHandler extends makeSingleton(CoreFileUploaderAudioHandlerService) { }
|
||||||
|
|
|
@ -40,9 +40,7 @@ import { CoreObject } from '@singletons/object';
|
||||||
/**
|
/**
|
||||||
* Helper provider that provides some common features regarding authentication.
|
* Helper provider that provides some common features regarding authentication.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreLoginHelperProvider {
|
export class CoreLoginHelperProvider {
|
||||||
|
|
||||||
static readonly OPEN_COURSE = 'open_course';
|
static readonly OPEN_COURSE = 'open_course';
|
||||||
|
|
|
@ -83,9 +83,7 @@ export interface CoreMainMenuHomeHandlerToDisplay extends CoreDelegateToDisplay,
|
||||||
* Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin
|
* Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin
|
||||||
* and notify an update in the data.
|
* and notify an update in the data.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreMainMenuHomeDelegateService extends CoreSortedDelegate<CoreMainMenuHomeHandlerToDisplay, CoreMainMenuHomeHandler> {
|
export class CoreMainMenuHomeDelegateService extends CoreSortedDelegate<CoreMainMenuHomeHandlerToDisplay, CoreMainMenuHomeHandler> {
|
||||||
|
|
||||||
protected featurePrefix = 'CoreMainMenuHomeDelegate_';
|
protected featurePrefix = 'CoreMainMenuHomeDelegate_';
|
||||||
|
|
|
@ -95,9 +95,7 @@ export interface CoreMainMenuHandlerToDisplay extends CoreDelegateToDisplay, Cor
|
||||||
* Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin
|
* Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin
|
||||||
* and notify an update in the data.
|
* and notify an update in the data.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreMainMenuDelegateService extends CoreSortedDelegate<CoreMainMenuHandlerToDisplay, CoreMainMenuHandler> {
|
export class CoreMainMenuDelegateService extends CoreSortedDelegate<CoreMainMenuHandlerToDisplay, CoreMainMenuHandler> {
|
||||||
|
|
||||||
protected featurePrefix = 'CoreMainMenuDelegate_';
|
protected featurePrefix = 'CoreMainMenuDelegate_';
|
||||||
|
|
|
@ -25,9 +25,7 @@ import { makeSingleton } from '@singletons';
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding Main Menu.
|
* Service that provides some features regarding Main Menu.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreMainMenuProvider {
|
export class CoreMainMenuProvider {
|
||||||
|
|
||||||
static readonly NUM_MAIN_HANDLERS = 4;
|
static readonly NUM_MAIN_HANDLERS = 4;
|
||||||
|
|
|
@ -24,4 +24,8 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,7 @@ import { makeSingleton } from '@singletons';
|
||||||
/**
|
/**
|
||||||
* Service that enables adding a history to a search box.
|
* Service that enables adding a history to a search box.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreSearchHistoryProvider {
|
export class CoreSearchHistoryProvider {
|
||||||
|
|
||||||
protected static readonly HISTORY_LIMIT = 10;
|
protected static readonly HISTORY_LIMIT = 10;
|
||||||
|
|
|
@ -63,9 +63,7 @@ export type CoreSettingsHandlerToDisplay = CoreDelegateToDisplay & CoreSettingsH
|
||||||
* Service to interact with addons to be shown in app settings. Provides functions to register a plugin
|
* Service to interact with addons to be shown in app settings. Provides functions to register a plugin
|
||||||
* and notify an update in the data.
|
* and notify an update in the data.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreSettingsDelegateService extends CoreSortedDelegate<CoreSettingsHandlerToDisplay, CoreSettingsHandler> {
|
export class CoreSettingsDelegateService extends CoreSortedDelegate<CoreSettingsHandlerToDisplay, CoreSettingsHandler> {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -48,9 +48,7 @@ export const enum CoreColorScheme {
|
||||||
/**
|
/**
|
||||||
* Settings helper service.
|
* Settings helper service.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreSettingsHelperProvider {
|
export class CoreSettingsHelperProvider {
|
||||||
|
|
||||||
protected syncPromises: { [s: string]: Promise<void> } = {};
|
protected syncPromises: { [s: string]: Promise<void> } = {};
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
||||||
<ng-template #allCourseList>
|
<ng-template #allCourseList>
|
||||||
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/all" detail>
|
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/main/home/courses/all" detail>
|
||||||
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
|
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'core.courses.availablecourses' | translate}}</h2>
|
<h2>{{ 'core.courses.availablecourses' | translate}}</h2>
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #categories>
|
<ng-template #categories>
|
||||||
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/categories" detail>
|
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/main/home/courses/categories" detail>
|
||||||
<ion-icon name="folder" slot="start"></ion-icon>
|
<ion-icon name="folder" slot="start"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'core.courses.categories' | translate}}</h2>
|
<h2>{{ 'core.courses.categories' | translate}}</h2>
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #enrolledCourseList>
|
<ng-template #enrolledCourseList>
|
||||||
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/my" detail>
|
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/main/home/courses/my" detail>
|
||||||
<ion-icon name="fas-graduation-cap" fixed-width slot="start">
|
<ion-icon name="fas-graduation-cap" fixed-width slot="start">
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
<ion-label><h2>{{ 'core.courses.mycourses' | translate}}</h2></ion-label>
|
<ion-label><h2>{{ 'core.courses.mycourses' | translate}}</h2></ion-label>
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #courseSearch>
|
<ng-template #courseSearch>
|
||||||
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/search" detail>
|
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/main/home/courses/search" detail>
|
||||||
<ion-icon name="fas-search" slot="start"></ion-icon>
|
<ion-icon name="fas-search" slot="start"></ion-icon>
|
||||||
<ion-label><h2>{{ 'core.courses.searchcourses' | translate}}</h2></ion-label>
|
<ion-label><h2>{{ 'core.courses.searchcourses' | translate}}</h2></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
|
@ -194,7 +194,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
||||||
* Open page to manage courses storage.
|
* Open page to manage courses storage.
|
||||||
*/
|
*/
|
||||||
manageCoursesStorage(): void {
|
manageCoursesStorage(): void {
|
||||||
// @todo this.navCtrl.navigateForward(['/courses/storage']);
|
// @todo this.navCtrl.navigateForward(['/main/home/courses/storage']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -35,9 +35,7 @@ export enum FrontPageItemNames {
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding site home.
|
* Service that provides some features regarding site home.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({ providedIn: 'root' })
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CoreSiteHomeProvider {
|
export class CoreSiteHomeProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"defautltagcoll": "Default collection",
|
||||||
|
"errorareanotsupported": "This tag area is not supported by the app.",
|
||||||
|
"inalltagcoll": "Everywhere",
|
||||||
|
"itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"",
|
||||||
|
"notagsfound": "No tags matching \"{{$a}}\" found",
|
||||||
|
"noresultsfor": "No results for \"{{$a}}\"",
|
||||||
|
"searchtags": "Search tags",
|
||||||
|
"showingfirsttags": "Showing {{$a}} most popular tags",
|
||||||
|
"tag": "Tag",
|
||||||
|
"tagarea_course": "Courses",
|
||||||
|
"tagarea_course_modules": "Activities and resources",
|
||||||
|
"tagarea_post": "Blog posts",
|
||||||
|
"tagarea_user": "User interests",
|
||||||
|
"tags": "Tags",
|
||||||
|
"warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app."
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<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.tag.itemstaggedwith' | translate: { $a: {tagarea: areaNameKey | translate, tag: tagName} } }}
|
||||||
|
</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<!-- @todo <ng-container *ngIf="loaded">
|
||||||
|
<core-dynamic-component [component]="areaComponent" [data]="{items: items}"></core-dynamic-component>
|
||||||
|
</ng-container>-->
|
||||||
|
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError">
|
||||||
|
</core-infinite-loading>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,47 @@
|
||||||
|
// (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 { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
|
||||||
|
import { CoreTagIndexAreaPage } from './index-area.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreTagIndexAreaPage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreTagIndexAreaPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreTagIndexAreaPageModule {}
|
|
@ -0,0 +1,175 @@
|
||||||
|
// (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, OnInit } from '@angular/core';
|
||||||
|
import { IonInfiniteScroll, IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreTag } from '@features/tag/services/tag';
|
||||||
|
import { CoreTagFeedElement } from '../../services/tag-helper';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { CoreTagAreaDelegate } from '../../services/tag-area-delegate';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the tag index area.
|
||||||
|
*
|
||||||
|
* @todo testing.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-tag-index-area',
|
||||||
|
templateUrl: 'index-area.html',
|
||||||
|
})
|
||||||
|
export class CoreTagIndexAreaPage implements OnInit {
|
||||||
|
|
||||||
|
tagId = 0;
|
||||||
|
tagName = '';
|
||||||
|
collectionId = 0;
|
||||||
|
areaId = 0;
|
||||||
|
fromContextId = 0;
|
||||||
|
contextId = 0;
|
||||||
|
recursive = true;
|
||||||
|
|
||||||
|
areaNameKey = '';
|
||||||
|
loaded = false;
|
||||||
|
componentName?: string;
|
||||||
|
itemType?: string;
|
||||||
|
items: CoreTagFeedElement[] = [];
|
||||||
|
nextPage = 0;
|
||||||
|
canLoadMore = false;
|
||||||
|
areaComponent: any; // @todo
|
||||||
|
loadMoreError = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View loaded.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
|
||||||
|
const navParams = this.route.snapshot.queryParamMap;
|
||||||
|
|
||||||
|
this.tagId = navParams['tagId'] ? parseInt(navParams['tagId'], 10) : this.tagId;
|
||||||
|
this.tagName = navParams['tagName'] || this.tagName;
|
||||||
|
this.collectionId = navParams['collectionId'] ? parseInt(navParams['collectionId'], 10) : this.collectionId;
|
||||||
|
this.areaId = navParams['areaId'] ? parseInt(navParams['areaId']!, 10) : this.areaId;
|
||||||
|
this.fromContextId = parseInt(navParams['fromContextId'], 10) || this.fromContextId;
|
||||||
|
this.contextId = navParams['contextId'] ? parseInt(navParams['contextId'], 10) : this.contextId;
|
||||||
|
this.recursive = typeof navParams['recursive'] == 'undefined'? true : navParams['recursive'];
|
||||||
|
|
||||||
|
this.areaNameKey = navParams['areaNameKey'];
|
||||||
|
// Pass the the following parameters to avoid fetching the first page.
|
||||||
|
this.componentName = navParams['componentName'];
|
||||||
|
this.itemType = navParams['itemType'];
|
||||||
|
this.items = []; // @todo navParams['items'] || [];
|
||||||
|
this.nextPage = navParams.has('nextPage') ? parseInt(navParams['nextPage']!, 10) : 0;
|
||||||
|
this.canLoadMore = !!navParams['canLoadMore'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.componentName || !this.itemType || !this.items.length || this.nextPage == 0) {
|
||||||
|
await this.fetchData(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.areaComponent = await CoreTagAreaDelegate.instance.getComponent(this.componentName!, this.itemType!);
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch next page of the tag index area.
|
||||||
|
*
|
||||||
|
* @param refresh Whether to refresh the data or fetch a new page.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
async fetchData(refresh: boolean = false): Promise<void> {
|
||||||
|
this.loadMoreError = false;
|
||||||
|
const page = refresh ? 0 : this.nextPage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const areas = await CoreTag.instance.getTagIndexPerArea(
|
||||||
|
this.tagId,
|
||||||
|
this.tagName,
|
||||||
|
this.collectionId,
|
||||||
|
this.areaId,
|
||||||
|
this.fromContextId,
|
||||||
|
this.contextId,
|
||||||
|
this.recursive,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
const area = areas[0];
|
||||||
|
|
||||||
|
const items = await CoreTagAreaDelegate.instance.parseContent(area.component, area.itemtype, area.content);
|
||||||
|
if (!items || !items.length) {
|
||||||
|
// Tag area not supported.
|
||||||
|
throw Translate.instance.instant('core.tag.errorareanotsupported');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == 0) {
|
||||||
|
this.items = items;
|
||||||
|
} else {
|
||||||
|
this.items.push(...items);
|
||||||
|
}
|
||||||
|
this.componentName = area.component;
|
||||||
|
this.itemType = area.itemtype;
|
||||||
|
this.areaNameKey = CoreTagAreaDelegate.instance.getDisplayNameKey(area.component, area.itemtype);
|
||||||
|
this.canLoadMore = !!area.nextpageurl;
|
||||||
|
this.nextPage = page + 1;
|
||||||
|
} catch (error) {
|
||||||
|
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading tag index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load more items.
|
||||||
|
*
|
||||||
|
* @param infiniteComplete Infinite scroll complete function.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
async loadMore(infiniteComplete?: CustomEvent<IonInfiniteScroll>): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.fetchData();
|
||||||
|
} finally {
|
||||||
|
infiniteComplete?.detail.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
*/
|
||||||
|
async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> {
|
||||||
|
try {
|
||||||
|
await CoreTag.instance.invalidateTagIndexPerArea(
|
||||||
|
this.tagId,
|
||||||
|
this.tagName,
|
||||||
|
this.collectionId,
|
||||||
|
this.areaId,
|
||||||
|
this.fromContextId,
|
||||||
|
this.contextId,
|
||||||
|
this.recursive,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await this.fetchData(true);
|
||||||
|
} finally {
|
||||||
|
refresher?.detail.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<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.tag.tag' | translate }}: {{ tagName }}</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<!--@todo <core-split-view>-->
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<ion-list *ngIf="hasUnsupportedAreas || areas.length">
|
||||||
|
<ion-item *ngIf="hasUnsupportedAreas" class="core-warning-item">
|
||||||
|
<ion-icon slot="start" name="fas-exclamation-triangle" color="warning"></ion-icon>
|
||||||
|
<ion-label class="ion-text-wrap">{{ 'core.tag.warningareasnotsupported' | translate }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" *ngFor="let area of areas" [title]="area.nameKey | translate"
|
||||||
|
(click)="openArea(area)" [class.core-split-item-selected]="area!.id == selectedAreaId">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ area!.nameKey | translate }}</h2>
|
||||||
|
</ion-label>
|
||||||
|
<ion-badge slot="end" *ngIf="area!.badge">{{ area!.badge }}</ion-badge>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
<core-empty-box icon="fa-tag" *ngIf="!hasUnsupportedAreas && (!areas || !areas.length)"
|
||||||
|
[message]="'core.tag.noresultsfor' | translate: { $a: tagName }"></core-empty-box>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,47 @@
|
||||||
|
// (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 { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
|
||||||
|
import { CoreTagIndexPage } from './index.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreTagIndexPage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreTagIndexPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreTagIndexPageModule {}
|
|
@ -0,0 +1,186 @@
|
||||||
|
// (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, OnInit } from '@angular/core';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
// import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
import { CoreTag } from '@features/tag/services/tag';
|
||||||
|
import { CoreTagAreaDelegate } from '@/core/features/tag/services/tag-area-delegate';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { CoreTagFeedElement } from '../../services/tag-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the tag index.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-tag-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class CoreTagIndexPage implements OnInit {
|
||||||
|
|
||||||
|
// @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||||
|
|
||||||
|
tagId = 0;
|
||||||
|
tagName = '';
|
||||||
|
collectionId = 0;
|
||||||
|
areaId = 0;
|
||||||
|
fromContextId = 0;
|
||||||
|
contextId = 0;
|
||||||
|
recursive = true;
|
||||||
|
loaded = false;
|
||||||
|
selectedAreaId?: number;
|
||||||
|
hasUnsupportedAreas = false;
|
||||||
|
|
||||||
|
areas: (CoreTagAreaDisplay | null)[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View loaded.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
const navParams = this.route.snapshot.queryParams;
|
||||||
|
|
||||||
|
this.tagId = navParams['tagId'] ? parseInt(navParams['tagId'], 10) : this.tagId;
|
||||||
|
this.tagName = navParams['tagName'] || this.tagName;
|
||||||
|
this.collectionId = navParams['collectionId'] ? parseInt(navParams['collectionId'], 10) : this.collectionId;
|
||||||
|
this.areaId = navParams['areaId'] ? parseInt(navParams['areaId']!, 10) : this.areaId;
|
||||||
|
this.fromContextId = parseInt(navParams['fromContextId'], 10) || this.fromContextId;
|
||||||
|
this.contextId = navParams['contextId'] ? parseInt(navParams['contextId'], 10) : this.contextId;
|
||||||
|
this.recursive = typeof navParams['recursive'] == 'undefined'? true : navParams['recursive'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchData();
|
||||||
|
/* if (this.splitviewCtrl.isOn() && this.areas && this.areas.length > 0) {
|
||||||
|
const area = this.areas.find((area) => area.id == this.areaId);
|
||||||
|
this.openArea(area || this.areas[0]);
|
||||||
|
}*/
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch first page of tag index per area.
|
||||||
|
*
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
async fetchData(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const areas = await CoreTag.instance.getTagIndexPerArea(
|
||||||
|
this.tagId,
|
||||||
|
this.tagName,
|
||||||
|
this.collectionId,
|
||||||
|
this.areaId,
|
||||||
|
this.fromContextId,
|
||||||
|
this.contextId,
|
||||||
|
this.recursive,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.areas = [];
|
||||||
|
this.hasUnsupportedAreas = false;
|
||||||
|
|
||||||
|
const areasDisplay: (CoreTagAreaDisplay | null)[] = await Promise.all(areas.map(async (area) => {
|
||||||
|
const items = await CoreTagAreaDelegate.instance.parseContent(area.component, area.itemtype, area.content);
|
||||||
|
|
||||||
|
if (!items || !items.length) {
|
||||||
|
// Tag area not supported, skip.
|
||||||
|
this.hasUnsupportedAreas = true;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: area.ta,
|
||||||
|
componentName: area.component,
|
||||||
|
itemType: area.itemtype,
|
||||||
|
nameKey: CoreTagAreaDelegate.instance.getDisplayNameKey(area.component, area.itemtype),
|
||||||
|
items,
|
||||||
|
canLoadMore: !!area.nextpageurl,
|
||||||
|
badge: items && items.length ? items.length + (area.nextpageurl ? '+' : '') : '',
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.areas = areasDisplay.filter((area) => area != null);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading tag index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshData(refresher?: CustomEvent<IonRefresher>): void {
|
||||||
|
CoreTag.instance.invalidateTagIndexPerArea(
|
||||||
|
this.tagId,
|
||||||
|
this.tagName,
|
||||||
|
this.collectionId,
|
||||||
|
this.areaId,
|
||||||
|
this.fromContextId,
|
||||||
|
this.contextId,
|
||||||
|
this.recursive,
|
||||||
|
).finally(() => {
|
||||||
|
this.fetchData().finally(() => {
|
||||||
|
refresher?.detail.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to an index area.
|
||||||
|
*
|
||||||
|
* @param area Area.
|
||||||
|
*/
|
||||||
|
openArea(area: CoreTagAreaDisplay): void {
|
||||||
|
this.selectedAreaId = area.id;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
tagId: this.tagId,
|
||||||
|
tagName: this.tagName,
|
||||||
|
collectionId: this.collectionId,
|
||||||
|
areaId: area.id,
|
||||||
|
fromContextId: this.fromContextId,
|
||||||
|
contextId: this.contextId,
|
||||||
|
recursive: this.recursive,
|
||||||
|
areaNameKey: area.nameKey,
|
||||||
|
componentName: area.componentName,
|
||||||
|
itemType: area.itemType,
|
||||||
|
items: area.items.slice(),
|
||||||
|
canLoadMore: area.canLoadMore,
|
||||||
|
nextPage: 1,
|
||||||
|
};
|
||||||
|
// this.splitviewCtrl.push('core-tag-index-area', params);
|
||||||
|
this.router.navigate(['core-tag-index-area'], { queryParams: params });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CoreTagAreaDisplay = {
|
||||||
|
id: number;
|
||||||
|
componentName: string;
|
||||||
|
itemType: string;
|
||||||
|
nameKey: string;
|
||||||
|
items: CoreTagFeedElement[];
|
||||||
|
canLoadMore: boolean;
|
||||||
|
badge: string;
|
||||||
|
};
|
|
@ -0,0 +1,45 @@
|
||||||
|
<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.tag.searchtags' | translate }}</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<ion-grid class="safe-area-page">
|
||||||
|
<ion-row>
|
||||||
|
<ion-col size="12" [attr.col-sm-6]="collections && collections.length > 1 ? '' : null">
|
||||||
|
<core-search-box (onSubmit)="searchTags($event)" (onClear)="searchTags('')" [initialSearch]="query"
|
||||||
|
[disabled]="searching" autocorrect="off" [spellcheck]="false" [autoFocus]="false" [lengthCheck]="0"
|
||||||
|
searchArea="CoreTag"></core-search-box>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col size="12" size-sm="6" *ngIf="collections && collections.length > 1">
|
||||||
|
<ion-select class="ion-text-start" [(ngModel)]="collectionId" (ngModelChange)="searchTags(query)"
|
||||||
|
[disabled]="searching" interface="popover" class="core-button-select">
|
||||||
|
<ion-select-option [value]="0">{{ 'core.tag.inalltagcoll' | translate }}</ion-select-option>
|
||||||
|
<ion-select-option *ngFor="let collection of collections" [value]="collection.id">
|
||||||
|
{{ collection.name }}</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
<core-loading [hideUntil]="loaded && !searching" class="safe-area-page">
|
||||||
|
<core-empty-box *ngIf="!cloud || !cloud!.tags || !cloud!.tags.length" icon="fas-tags"
|
||||||
|
[message]="'core.tag.notagsfound' | translate: {$a: query}"></core-empty-box>
|
||||||
|
|
||||||
|
<ng-container *ngIf="cloud && cloud!.tags && cloud!.tags.length > 0">
|
||||||
|
<div class="ion-text-center core-tag-cloud">
|
||||||
|
<ion-badge *ngFor="let tag of cloud!.tags" (click)="openTag(tag)" class="ion-text-wrap">
|
||||||
|
<span [class]="'size' + tag.size">{{ tag.name }}</span>
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
<p *ngIf="cloud!.tags.length < cloud!.totalcount" class="ion-text-center">
|
||||||
|
{{ 'core.tag.showingfirsttags' | translate: {$a: cloud!.tags.length} }}
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,50 @@
|
||||||
|
// (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 { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||||
|
|
||||||
|
import { CoreTagSearchPage } from './search.page';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreTagSearchPage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreTagSearchPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreTagSearchPageModule {}
|
|
@ -0,0 +1,142 @@
|
||||||
|
// (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, OnInit } from '@angular/core';
|
||||||
|
import { IonRefresher, NavController } from '@ionic/angular';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreTagCloud, CoreTagCollection, CoreTagCloudTag, CoreTag } from '@features/tag/services/tag';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays most used tags and allows searching.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-tag-search',
|
||||||
|
templateUrl: 'search.html',
|
||||||
|
styleUrls: ['search.scss'],
|
||||||
|
})
|
||||||
|
export class CoreTagSearchPage implements OnInit {
|
||||||
|
|
||||||
|
collectionId!: number;
|
||||||
|
query!: string;
|
||||||
|
collections: CoreTagCollection[] = [];
|
||||||
|
cloud?: CoreTagCloud;
|
||||||
|
loaded = false;
|
||||||
|
searching = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected navCtrl: NavController,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View loaded.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
// @todo: Check params work.
|
||||||
|
this.collectionId = this.route.snapshot.queryParamMap.has('collectionId') ?
|
||||||
|
parseInt(this.route.snapshot.queryParamMap.get('collectionId')!, 10) : 0;
|
||||||
|
this.query = this.route.snapshot.queryParamMap.get('query') || '';
|
||||||
|
|
||||||
|
this.fetchData().finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchData(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
this.fetchCollections(),
|
||||||
|
this.fetchTags(),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading tags.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch tag collections.
|
||||||
|
*
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
async fetchCollections(): Promise<void> {
|
||||||
|
const collections = await CoreTag.instance.getTagCollections();
|
||||||
|
|
||||||
|
collections.forEach((collection) => {
|
||||||
|
if (!collection.name && collection.isdefault) {
|
||||||
|
collection.name = Translate.instance.instant('core.tag.defautltagcoll');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.collections = collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch tags.
|
||||||
|
*
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
async fetchTags(): Promise<void> {
|
||||||
|
this.cloud = await CoreTag.instance.getTagCloud(this.collectionId, undefined, undefined, this.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to tag index page.
|
||||||
|
*/
|
||||||
|
openTag(tag: CoreTagCloudTag): void {
|
||||||
|
const url = CoreTextUtils.instance.decodeURI(tag.viewurl);
|
||||||
|
CoreContentLinksHelper.instance.handleLink(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher event.
|
||||||
|
*/
|
||||||
|
refreshData(refresher?: CustomEvent<IonRefresher>): void {
|
||||||
|
CoreUtils.instance.allPromises([
|
||||||
|
CoreTag.instance.invalidateTagCollections(),
|
||||||
|
CoreTag.instance.invalidateTagCloud(this.collectionId, undefined, undefined, this.query),
|
||||||
|
]).finally(() => this.fetchData().finally(() => {
|
||||||
|
refresher?.detail.complete();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search tags.
|
||||||
|
*
|
||||||
|
* @param query Search query.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
searchTags(query: string): Promise<void> {
|
||||||
|
this.searching = true;
|
||||||
|
this.query = query;
|
||||||
|
CoreApp.instance.closeKeyboard();
|
||||||
|
|
||||||
|
return this.fetchTags().catch((error) => {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading tags.');
|
||||||
|
}).finally(() => {
|
||||||
|
this.searching = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
:host {
|
||||||
|
core-search-box ion-card {
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-tag-cloud ion-badge {
|
||||||
|
margin: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.size20 {
|
||||||
|
font-size: 3.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size19 {
|
||||||
|
font-size: 3.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size18 {
|
||||||
|
font-size: 3.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size17 {
|
||||||
|
font-size: 3.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size16 {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size15 {
|
||||||
|
font-size: 2.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size14 {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size13 {
|
||||||
|
font-size: 2.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size12 {
|
||||||
|
font-size: 2.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size11 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size10 {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size9 {
|
||||||
|
font-size: 2.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size8 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size7 {
|
||||||
|
font-size: 2.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size6 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size5 {
|
||||||
|
font-size: 1.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size4 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size3 {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size2 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size0 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||||
|
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreTag } from '../tag';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to tag index.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreTagIndexLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||||
|
|
||||||
|
name = 'CoreTagIndexLinkHandler';
|
||||||
|
pattern = /\/tag\/index\.php/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of actions for a link (url).
|
||||||
|
*
|
||||||
|
* @param siteIds List of sites the URL belongs to.
|
||||||
|
* @param url The URL to treat.
|
||||||
|
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||||
|
* @param courseId Course ID related to the URL. Optional but recommended.
|
||||||
|
* @param data Extra data to handle the URL.
|
||||||
|
* @return List of (or promise resolved with list of) actions.
|
||||||
|
*/
|
||||||
|
getActions(
|
||||||
|
siteIds: string[],
|
||||||
|
url: string,
|
||||||
|
params: Params,
|
||||||
|
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||||
|
return [{
|
||||||
|
action: (siteId): void => {
|
||||||
|
const pageParams = {
|
||||||
|
tagId: parseInt(params.id, 10) || 0,
|
||||||
|
tagName: params.tag || '',
|
||||||
|
collectionId: parseInt(params.tc, 10) || 0,
|
||||||
|
areaId: parseInt(params.ta, 10) || 0,
|
||||||
|
fromContextId: parseInt(params.from, 10) || 0,
|
||||||
|
contextId: parseInt(params.ctx, 10) || 0,
|
||||||
|
recursive: parseInt(params.rec, 10) || 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pageParams.tagId && (!pageParams.tagName || !pageParams.collectionId)) {
|
||||||
|
CoreContentLinksHelper.instance.goInSite('/main/tag/search', {}, siteId);
|
||||||
|
} else if (pageParams.areaId) {
|
||||||
|
CoreContentLinksHelper.instance.goInSite('/main/tag/index-area', pageParams, siteId);
|
||||||
|
} else {
|
||||||
|
CoreContentLinksHelper.instance.goInSite('/main/tag/index', pageParams, siteId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled for a certain site (site + user) and a URL.
|
||||||
|
* If not defined, defaults to true.
|
||||||
|
*
|
||||||
|
* @param siteId The site ID.
|
||||||
|
* @param url The URL to treat.
|
||||||
|
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||||
|
* @param courseId Course ID related to the URL. Optional but recommended.
|
||||||
|
* @return Whether the handler is enabled for the URL and site.
|
||||||
|
*/
|
||||||
|
isEnabled(siteId: string): boolean | Promise<boolean> {
|
||||||
|
return CoreTag.instance.areTagsAvailable(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreTagIndexLinkHandler extends makeSingleton(CoreTagIndexLinkHandlerService) {}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||||
|
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreTag } from '../tag';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to tag search.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreTagSearchLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||||
|
|
||||||
|
name = 'CoreTagSearchLinkHandler';
|
||||||
|
pattern = /\/tag\/search\.php/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of actions for a link (url).
|
||||||
|
*
|
||||||
|
* @param siteIds List of sites the URL belongs to.
|
||||||
|
* @param url The URL to treat.
|
||||||
|
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||||
|
* @param courseId Course ID related to the URL. Optional but recommended.
|
||||||
|
* @param data Extra data to handle the URL.
|
||||||
|
* @return List of (or promise resolved with list of) actions.
|
||||||
|
*/
|
||||||
|
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||||
|
return [{
|
||||||
|
action: (siteId): void => {
|
||||||
|
const pageParams = {
|
||||||
|
collectionId: parseInt(params.tc, 10) || 0,
|
||||||
|
query: params.query || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
CoreContentLinksHelper.instance.goInSite('/main/tag/search', pageParams, siteId);
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled for a certain site (site + user) and a URL.
|
||||||
|
* If not defined, defaults to true.
|
||||||
|
*
|
||||||
|
* @param siteId The site ID.
|
||||||
|
* @return Whether the handler is enabled for the URL and site.
|
||||||
|
*/
|
||||||
|
isEnabled(siteId: string): boolean | Promise<boolean> {
|
||||||
|
return CoreTag.instance.areTagsAvailable(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreTagSearchLinkHandler extends makeSingleton(CoreTagSearchLinkHandlerService) {}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreTag } from '../tag';
|
||||||
|
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to inject an option into main menu.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreTagMainMenuHandlerService implements CoreMainMenuHandler {
|
||||||
|
|
||||||
|
static readonly PAGE_NAME = 'tag';
|
||||||
|
|
||||||
|
name = 'CoreTag';
|
||||||
|
priority = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
const available = await CoreTag.instance.areTagsAvailable();
|
||||||
|
if (!available) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The only way to check whether tags are enabled on web is to perform a WS call.
|
||||||
|
return CoreUtils.instance.promiseWorks(CoreTag.instance.getTagCollections());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data needed to render the handler.
|
||||||
|
*
|
||||||
|
* @return Data needed to render the handler.
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreMainMenuHandlerData {
|
||||||
|
return {
|
||||||
|
icon: 'fas-tags',
|
||||||
|
title: 'core.tag.tags',
|
||||||
|
page: CoreTagMainMenuHandlerService.PAGE_NAME,
|
||||||
|
class: 'core-tag-search-handler',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreTagMainMenuHandler extends makeSingleton(CoreTagMainMenuHandlerService) {}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// (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 { Injectable, Type } from '@angular/core';
|
||||||
|
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreTagFeedElement } from './tag-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that all tag area handlers must implement.
|
||||||
|
*/
|
||||||
|
export interface CoreTagAreaHandler extends CoreDelegateHandler {
|
||||||
|
/**
|
||||||
|
* Component and item type separated by a slash. E.g. 'core/course_modules'.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the rendered content of a tag index and returns the items.
|
||||||
|
*
|
||||||
|
* @param content Rendered content.
|
||||||
|
* @return Area items (or promise resolved with the items).
|
||||||
|
*/
|
||||||
|
parseContent(content: string): CoreTagFeedElement[] | Promise<CoreTagFeedElement[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the component to use to display items.
|
||||||
|
*
|
||||||
|
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
* @todo, check return types.
|
||||||
|
*/
|
||||||
|
getComponent(): Type<unknown> | Promise<Type<unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate to register tag area handlers.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreTagAreaDelegateService extends CoreDelegate<CoreTagAreaHandler> {
|
||||||
|
|
||||||
|
protected handlerNameProperty = 'type';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('CoreTagAreaDelegate');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the display name string for this area.
|
||||||
|
*
|
||||||
|
* @param component Component name.
|
||||||
|
* @param itemType Item type.
|
||||||
|
* @return String key.
|
||||||
|
*/
|
||||||
|
getDisplayNameKey(component: string, itemType: string): string {
|
||||||
|
return (component == 'core' ? 'core.tag' : 'addon.' + component) + '.tagarea_' + itemType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the rendered content of a tag index and returns the items.
|
||||||
|
*
|
||||||
|
* @param component Component name.
|
||||||
|
* @param itemType Item type.
|
||||||
|
* @param content Rendered content.
|
||||||
|
* @return Promise resolved with the area items, or undefined if not found.
|
||||||
|
*/
|
||||||
|
async parseContent(component: string, itemType: string, content: string): Promise<CoreTagFeedElement[] | undefined> {
|
||||||
|
const type = component + '/' + itemType;
|
||||||
|
|
||||||
|
return await this.executeFunctionOnEnabled(type, 'parseContent', [content]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the component to use to display an area item.
|
||||||
|
*
|
||||||
|
* @param component Component name.
|
||||||
|
* @param itemType Item type.
|
||||||
|
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
* @todo, check return types.
|
||||||
|
*/
|
||||||
|
async getComponent(component: string, itemType: string): Promise<Type<unknown> | undefined> {
|
||||||
|
const type = component + '/' + itemType;
|
||||||
|
|
||||||
|
return await this.executeFunctionOnEnabled(type, 'getComponent');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreTagAreaDelegate extends makeSingleton(CoreTagAreaDelegateService) {}
|
|
@ -0,0 +1,94 @@
|
||||||
|
// (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 { makeSingleton } from '@singletons';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service with helper functions for tags.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreTagHelperProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the rendered content of the "core_tag/tagfeed" web template and returns the items.
|
||||||
|
*
|
||||||
|
* @param content Rendered content.
|
||||||
|
* @return Area items.
|
||||||
|
*/
|
||||||
|
parseFeedContent(content: string): CoreTagFeedElement[] {
|
||||||
|
const items: CoreTagFeedElement[] = [];
|
||||||
|
const element = CoreDomUtils.instance.convertToElement(content);
|
||||||
|
|
||||||
|
Array.from(element.querySelectorAll('ul.tag_feed > li.media')).forEach((itemElement) => {
|
||||||
|
const item: CoreTagFeedElement = { details: [] };
|
||||||
|
|
||||||
|
Array.from(itemElement.querySelectorAll('div.media-body > div')).forEach((div: HTMLElement) => {
|
||||||
|
if (div.classList.contains('media-heading')) {
|
||||||
|
item.heading = div.innerText.trim();
|
||||||
|
const link = div.querySelector('a');
|
||||||
|
if (link) {
|
||||||
|
item.url = link.getAttribute('href');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Separate details by lines.
|
||||||
|
const lines = [''];
|
||||||
|
Array.from(div.childNodes).forEach((childNode: Node) => {
|
||||||
|
if (childNode.nodeType == Node.TEXT_NODE) {
|
||||||
|
lines[lines.length - 1] += childNode.textContent;
|
||||||
|
} else if (childNode.nodeType == Node.ELEMENT_NODE) {
|
||||||
|
const childElement = childNode as HTMLElement;
|
||||||
|
if (childElement.tagName == 'BR') {
|
||||||
|
lines.push('');
|
||||||
|
} else {
|
||||||
|
lines[lines.length - 1] += childElement.innerText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
item.details.push(...lines.map((line) => line.trim()).filter((line) => line != ''));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const image = itemElement.querySelector('div.itemimage img');
|
||||||
|
if (image) {
|
||||||
|
if (image.classList.contains('userpicture')) {
|
||||||
|
item.avatarUrl = image.getAttribute('src');
|
||||||
|
} else {
|
||||||
|
item.iconUrl = image.getAttribute('src');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.heading && item.url) {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreTagHelper extends makeSingleton(CoreTagHelperProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feed area element type.
|
||||||
|
*/
|
||||||
|
export type CoreTagFeedElement = {
|
||||||
|
details: string[];
|
||||||
|
heading?: string;
|
||||||
|
iconUrl?: string | null;
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
|
@ -0,0 +1,440 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||||
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
|
||||||
|
const ROOT_CACHE_KEY = 'CoreTag:';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle tags.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreTagProvider {
|
||||||
|
|
||||||
|
static readonly SEARCH_LIMIT = 150;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether tags are available in a certain site.
|
||||||
|
*
|
||||||
|
* @param siteId Site Id. If not defined, use current site.
|
||||||
|
* @return Promise resolved with true if available, resolved with false otherwise.
|
||||||
|
* @since 3.7
|
||||||
|
*/
|
||||||
|
async areTagsAvailable(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return this.areTagsAvailableInSite(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether tags are available in a certain site.
|
||||||
|
*
|
||||||
|
* @param site Site. If not defined, use current site.
|
||||||
|
* @return True if available.
|
||||||
|
*/
|
||||||
|
areTagsAvailableInSite(site?: CoreSite): boolean {
|
||||||
|
site = site || CoreSites.instance.getCurrentSite();
|
||||||
|
|
||||||
|
return !!site && site.wsAvailable('core_tag_get_tagindex_per_area') &&
|
||||||
|
site.wsAvailable('core_tag_get_tag_cloud') &&
|
||||||
|
site.wsAvailable('core_tag_get_tag_collections') &&
|
||||||
|
!site.isFeatureDisabled('NoDelegate_CoreTag');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the tag cloud.
|
||||||
|
*
|
||||||
|
* @param collectionId Tag collection ID.
|
||||||
|
* @param isStandard Whether to return only standard tags.
|
||||||
|
* @param sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
|
||||||
|
* @param search Search string.
|
||||||
|
* @param fromContextId Context ID where this tag cloud is displayed.
|
||||||
|
* @param contextId Only retrieve tag instances in this context.
|
||||||
|
* @param recursive Retrieve tag instances in the context and its children.
|
||||||
|
* @param limit Maximum number of tags to retrieve. Defaults to SEARCH_LIMIT.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the tag cloud.
|
||||||
|
* @since 3.7
|
||||||
|
*/
|
||||||
|
async getTagCloud(
|
||||||
|
collectionId: number = 0,
|
||||||
|
isStandard: boolean = false,
|
||||||
|
sort: string = 'name',
|
||||||
|
search: string = '',
|
||||||
|
fromContextId: number = 0,
|
||||||
|
contextId: number = 0,
|
||||||
|
recursive: boolean = true,
|
||||||
|
limit?: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<CoreTagCloud> {
|
||||||
|
limit = limit || CoreTagProvider.SEARCH_LIMIT;
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
const params: CoreTagGetTagCloudWSParams = {
|
||||||
|
tagcollid: collectionId,
|
||||||
|
isstandard: isStandard,
|
||||||
|
limit,
|
||||||
|
sort,
|
||||||
|
search,
|
||||||
|
fromctx: fromContextId,
|
||||||
|
ctx: contextId,
|
||||||
|
rec: recursive,
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
|
cacheKey: this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive),
|
||||||
|
getFromCache: search != '', // Try to get updated data when searching.
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.read('core_tag_get_tag_cloud', params, preSets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the tag collections.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the tag collections.
|
||||||
|
* @since 3.7
|
||||||
|
*/
|
||||||
|
async getTagCollections(siteId?: string): Promise<CoreTagCollection[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
|
cacheKey: this.getTagCollectionsKey(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: CoreTagCollections = await site.read('core_tag_get_tag_collections', null, preSets);
|
||||||
|
|
||||||
|
if (!response || !response.collections) {
|
||||||
|
throw null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the tag index.
|
||||||
|
*
|
||||||
|
* @param id Tag ID.
|
||||||
|
* @param name Tag name.
|
||||||
|
* @param collectionId Tag collection ID.
|
||||||
|
* @param areaId Tag area ID.
|
||||||
|
* @param fromContextId Context ID where the link was displayed.
|
||||||
|
* @param contextId Context ID where to search for items.
|
||||||
|
* @param recursive Search in the context and its children.
|
||||||
|
* @param page Page number.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the tag index per area.
|
||||||
|
* @since 3.7
|
||||||
|
*/
|
||||||
|
async getTagIndexPerArea(
|
||||||
|
id: number,
|
||||||
|
name: string = '',
|
||||||
|
collectionId: number = 0,
|
||||||
|
areaId: number = 0,
|
||||||
|
fromContextId: number = 0,
|
||||||
|
contextId: number = 0,
|
||||||
|
recursive: boolean = true,
|
||||||
|
page: number = 0,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<CoreTagIndex[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
const params: CoreTagGetTagindexPerAreaWSParams = {
|
||||||
|
tagindex: {
|
||||||
|
id,
|
||||||
|
tag: name,
|
||||||
|
tc: collectionId,
|
||||||
|
ta: areaId,
|
||||||
|
excl: true,
|
||||||
|
from: fromContextId,
|
||||||
|
ctx: contextId,
|
||||||
|
rec: recursive,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
||||||
|
cacheKey: this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response: CoreTagIndex[];
|
||||||
|
try {
|
||||||
|
response = await site.read('core_tag_get_tagindex_per_area', params, preSets);
|
||||||
|
} catch (error) {
|
||||||
|
// Workaround for WS not passing parameter to error string.
|
||||||
|
if (error && error.errorcode == 'notagsfound') {
|
||||||
|
error.message = Translate.instance.instant('core.tag.notagsfound', { $a: name || id || '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate tag cloud.
|
||||||
|
*
|
||||||
|
* @param collectionId Tag collection ID.
|
||||||
|
* @param isStandard Whether to return only standard tags.
|
||||||
|
* @param sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
|
||||||
|
* @param search Search string.
|
||||||
|
* @param fromContextId Context ID where this tag cloud is displayed.
|
||||||
|
* @param contextId Only retrieve tag instances in this context.
|
||||||
|
* @param recursive Retrieve tag instances in the context and its children.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateTagCloud(
|
||||||
|
collectionId: number = 0,
|
||||||
|
isStandard: boolean = false,
|
||||||
|
sort: string = 'name',
|
||||||
|
search: string = '',
|
||||||
|
fromContextId: number = 0,
|
||||||
|
contextId: number = 0,
|
||||||
|
recursive: boolean = true,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
const key = this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive);
|
||||||
|
|
||||||
|
return site.invalidateWsCacheForKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate tag collections.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateTagCollections(siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
const key = this.getTagCollectionsKey();
|
||||||
|
|
||||||
|
return site.invalidateWsCacheForKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate tag index.
|
||||||
|
*
|
||||||
|
* @param id Tag ID.
|
||||||
|
* @param name Tag name.
|
||||||
|
* @param collectionId Tag collection ID.
|
||||||
|
* @param areaId Tag area ID.
|
||||||
|
* @param fromContextId Context ID where the link was displayed.
|
||||||
|
* @param contextId Context ID where to search for items.
|
||||||
|
* @param recursive Search in the context and its children.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateTagIndexPerArea(
|
||||||
|
id: number,
|
||||||
|
name: string = '',
|
||||||
|
collectionId: number = 0,
|
||||||
|
areaId: number = 0,
|
||||||
|
fromContextId: number = 0,
|
||||||
|
contextId: number = 0,
|
||||||
|
recursive: boolean = true,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
const key = this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive);
|
||||||
|
|
||||||
|
return site.invalidateWsCacheForKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for tag cloud.
|
||||||
|
*
|
||||||
|
* @param collectionId Tag collection ID.
|
||||||
|
* @param isStandard Whether to return only standard tags.
|
||||||
|
* @param sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
|
||||||
|
* @param search Search string.
|
||||||
|
* @param fromContextId Context ID where this tag cloud is displayed.
|
||||||
|
* @param contextId Only retrieve tag instances in this context.
|
||||||
|
* @param recursive Retrieve tag instances in the context and it's children.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getTagCloudKey(
|
||||||
|
collectionId: number,
|
||||||
|
isStandard: boolean,
|
||||||
|
sort: string,
|
||||||
|
search: string,
|
||||||
|
fromContextId: number,
|
||||||
|
contextId: number,
|
||||||
|
recursive: boolean,
|
||||||
|
): string {
|
||||||
|
return ROOT_CACHE_KEY +
|
||||||
|
'cloud:' +
|
||||||
|
collectionId + ':' +
|
||||||
|
(isStandard ? 1 : 0) + ':' +
|
||||||
|
sort + ':' + search + ':' +
|
||||||
|
fromContextId + ':' +
|
||||||
|
contextId + ':' +
|
||||||
|
(recursive ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for tag collections.
|
||||||
|
*
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getTagCollectionsKey(): string {
|
||||||
|
return ROOT_CACHE_KEY + 'collections';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for tag index.
|
||||||
|
*
|
||||||
|
* @param id Tag ID.
|
||||||
|
* @param name Tag name.
|
||||||
|
* @param collectionId Tag collection ID.
|
||||||
|
* @param areaId Tag area ID.
|
||||||
|
* @param fromContextId Context ID where the link was displayed.
|
||||||
|
* @param contextId Context ID where to search for items.
|
||||||
|
* @param recursive Search in the context and its children.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getTagIndexPerAreaKey(
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
collectionId: number,
|
||||||
|
areaId: number,
|
||||||
|
fromContextId: number,
|
||||||
|
contextId: number,
|
||||||
|
recursive: boolean,
|
||||||
|
): string {
|
||||||
|
return ROOT_CACHE_KEY +
|
||||||
|
'index:' + id + ':' +
|
||||||
|
name + ':' + collectionId + ':' +
|
||||||
|
areaId + ':' + fromContextId + ':' +
|
||||||
|
contextId + ':' +
|
||||||
|
(recursive ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreTag extends makeSingleton(CoreTagProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of core_tag_get_tag_cloud WS.
|
||||||
|
*/
|
||||||
|
export type CoreTagGetTagCloudWSParams = {
|
||||||
|
tagcollid?: number; // Tag collection id.
|
||||||
|
isstandard?: boolean; // Whether to return only standard tags.
|
||||||
|
limit?: number; // Maximum number of tags to retrieve.
|
||||||
|
sort?: string; // Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid).
|
||||||
|
search?: string; // Search string.
|
||||||
|
fromctx?: number; // Context id where this tag cloud is displayed.
|
||||||
|
ctx?: number; // Only retrieve tag instances in this context.
|
||||||
|
rec?: boolean; // Retrieve tag instances in the $ctx context and it's children.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of a tag cloud returned by WS.
|
||||||
|
*/
|
||||||
|
export type CoreTagCloud = {
|
||||||
|
tags: CoreTagCloudTag[];
|
||||||
|
tagscount: number;
|
||||||
|
totalcount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of a tag cloud tag returned by WS.
|
||||||
|
*/
|
||||||
|
export type CoreTagCloudTag = {
|
||||||
|
name: string;
|
||||||
|
viewurl: string;
|
||||||
|
flag: boolean;
|
||||||
|
isstandard: boolean;
|
||||||
|
count: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of a tag collection returned by WS.
|
||||||
|
*/
|
||||||
|
export type CoreTagCollection = {
|
||||||
|
id: number; // Collection id.
|
||||||
|
name: string; // Collection name.
|
||||||
|
isdefault: boolean; // Whether is the default collection.
|
||||||
|
component: string; // Component the collection is related to.
|
||||||
|
sortorder: number; // Collection ordering in the list.
|
||||||
|
searchable: boolean; // Whether the tag collection is searchable.
|
||||||
|
customurl: string; // Custom URL for the tag page instead of /tag/index.php.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of tag collections returned by WS.
|
||||||
|
*/
|
||||||
|
export type CoreTagCollections = {
|
||||||
|
collections: CoreTagCollection[];
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of core_tag_get_tagindex_per_area WS.
|
||||||
|
*/
|
||||||
|
export type CoreTagGetTagindexPerAreaWSParams = {
|
||||||
|
tagindex: {
|
||||||
|
id?: number; // Tag id.
|
||||||
|
tag?: string; // Tag name.
|
||||||
|
tc?: number; // Tag collection id.
|
||||||
|
ta?: number; // Tag area id.
|
||||||
|
excl?: boolean; // Exlusive mode for this tag area.
|
||||||
|
from?: number; // Context id where the link was displayed.
|
||||||
|
ctx?: number; // Context id where to search for items.
|
||||||
|
rec?: boolean; // Search in the context recursive.
|
||||||
|
page?: number; // Page number (0-based).
|
||||||
|
}; // Parameters.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of a tag index returned by WS.
|
||||||
|
*/
|
||||||
|
export type CoreTagIndex = {
|
||||||
|
tagid: number;
|
||||||
|
ta: number;
|
||||||
|
component: string;
|
||||||
|
itemtype: string;
|
||||||
|
nextpageurl: string;
|
||||||
|
prevpageurl: string;
|
||||||
|
exclusiveurl: string;
|
||||||
|
exclusivetext: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
hascontent: number;
|
||||||
|
anchor: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of a tag item returned by WS.
|
||||||
|
*/
|
||||||
|
export type CoreTagItem = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
rawname: string;
|
||||||
|
isstandard: boolean;
|
||||||
|
tagcollid: number;
|
||||||
|
taginstanceid: number;
|
||||||
|
taginstancecontextid: number;
|
||||||
|
itemid: number;
|
||||||
|
ordering: number;
|
||||||
|
flag: number;
|
||||||
|
};
|
|
@ -0,0 +1,42 @@
|
||||||
|
// (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';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
loadChildren: () => import('@features/tag/pages/index/index.page.module').then(m => m.CoreTagIndexPageModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'search',
|
||||||
|
loadChildren: () => import('@features/tag//pages/search/search.page.module').then(m => m.CoreTagSearchPageModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'index-area',
|
||||||
|
loadChildren: () => import('@features/tag/pages/index-area/index-area.page.module').then(m => m.CoreTagIndexAreaPageModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'search',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreTagLazyModule { }
|
|
@ -0,0 +1,48 @@
|
||||||
|
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
|
||||||
|
import { CoreMainMenuRoutingModule } from '../mainmenu/mainmenu-routing.module';
|
||||||
|
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||||
|
import { CoreTagMainMenuHandler, CoreTagMainMenuHandlerService } from './services/handlers/tag.mainmenu';
|
||||||
|
import { CoreTagIndexLinkHandler } from './services/handlers/index.link';
|
||||||
|
import { CoreTagSearchLinkHandler } from './services/handlers/search.link';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: CoreTagMainMenuHandlerService.PAGE_NAME,
|
||||||
|
loadChildren: () => import('./tag-lazy.module').then(m => m.CoreTagLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreMainMenuRoutingModule.forChild({ children: routes }),
|
||||||
|
],
|
||||||
|
exports: [CoreMainMenuRoutingModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
useValue: () => {
|
||||||
|
CoreMainMenuDelegate.instance.registerHandler(CoreTagMainMenuHandler.instance);
|
||||||
|
CoreContentLinksDelegate.instance.registerHandler(CoreTagIndexLinkHandler.instance);
|
||||||
|
CoreContentLinksDelegate.instance.registerHandler(CoreTagSearchLinkHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreTagModule {}
|
|
@ -104,6 +104,9 @@
|
||||||
ion-toolbar {
|
ion-toolbar {
|
||||||
--color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
|
--color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
|
||||||
--background: var(--ion-statusbar-background);
|
--background: var(--ion-statusbar-background);
|
||||||
|
ion-button {
|
||||||
|
--ion-toolbar-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
ion-spinner {
|
ion-spinner {
|
||||||
--color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
|
--color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
|
||||||
|
|
Loading…
Reference in New Issue