commit
67f8b32c42
|
@ -0,0 +1,46 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { AddonModBookComponentsModule } from './components/components.module';
|
||||
import { AddonModBookProvider } from './providers/book';
|
||||
import { AddonModBookModuleHandler } from './providers/module-handler';
|
||||
import { AddonModBookLinkHandler } from './providers/link-handler';
|
||||
import { AddonModBookPrefetchHandler } from './providers/prefetch-handler';
|
||||
import { CoreCourseModuleDelegate } from '../../../core/course/providers/module-delegate';
|
||||
import { CoreContentLinksDelegate } from '../../../core/contentlinks/providers/delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '../../../core/course/providers/module-prefetch-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
imports: [
|
||||
AddonModBookComponentsModule
|
||||
],
|
||||
providers: [
|
||||
AddonModBookProvider,
|
||||
AddonModBookModuleHandler,
|
||||
AddonModBookLinkHandler,
|
||||
AddonModBookPrefetchHandler
|
||||
]
|
||||
})
|
||||
export class AddonModBookModule {
|
||||
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModBookModuleHandler,
|
||||
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModBookLinkHandler,
|
||||
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModBookPrefetchHandler) {
|
||||
moduleDelegate.registerHandler(moduleHandler);
|
||||
contentLinksDelegate.registerHandler(linkHandler);
|
||||
prefetchDelegate.registerHandler(prefetchHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '../../../../components/components.module';
|
||||
import { CoreDirectivesModule } from '../../../../directives/directives.module';
|
||||
import { CoreCourseComponentsModule } from '../../../../core/course/components/components.module';
|
||||
import { AddonModBookIndexComponent } from './index/index';
|
||||
import { AddonModBookTocPopoverComponent } from './toc-popover/toc-popover';
|
||||
import { AddonModBookNavigationArrowsComponent } from './navigation-arrows/navigation-arrows';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModBookIndexComponent,
|
||||
AddonModBookTocPopoverComponent,
|
||||
AddonModBookNavigationArrowsComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCourseComponentsModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModBookIndexComponent,
|
||||
AddonModBookTocPopoverComponent,
|
||||
AddonModBookNavigationArrowsComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModBookIndexComponent,
|
||||
AddonModBookTocPopoverComponent
|
||||
]
|
||||
})
|
||||
export class AddonModBookComponentsModule {}
|
|
@ -0,0 +1,26 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons end>
|
||||
<button ion-button icon-only (click)="showToc($event)">
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
</button>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
|
||||
<core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="500" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded">
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
|
||||
|
||||
<div padding>
|
||||
<addon-mod-book-navigation-arrows [previous]="previousChapter" [next]="nextChapter" (action)="changeChapter($event)"></addon-mod-book-navigation-arrows>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent"></core-format-text>
|
||||
<addon-mod-book-navigation-arrows [previous]="previousChapter" [next]="nextChapter" (action)="changeChapter($event)"></addon-mod-book-navigation-arrows>
|
||||
</div>
|
||||
|
||||
</core-loading>
|
|
@ -0,0 +1,250 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, OnDestroy, Input, Output, EventEmitter, Optional } from '@angular/core';
|
||||
import { NavParams, NavController, Content, PopoverController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '../../../../../providers/app';
|
||||
import { CoreDomUtilsProvider } from '../../../../../providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '../../../../../providers/utils/text';
|
||||
import { CoreCourseProvider } from '../../../../../core/course/providers/course';
|
||||
import { CoreCourseHelperProvider } from '../../../../../core/course/providers/helper';
|
||||
import { CoreCourseModuleMainComponent } from '../../../../../core/course/providers/module-delegate';
|
||||
import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book';
|
||||
import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler';
|
||||
import { AddonModBookTocPopoverComponent } from '../../components/toc-popover/toc-popover';
|
||||
|
||||
/**
|
||||
* Component that displays a book.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-book-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModBookIndexComponent implements OnInit, OnDestroy, CoreCourseModuleMainComponent {
|
||||
@Input() module: any; // The module of the book.
|
||||
@Input() courseId: number; // Course ID the book belongs to.
|
||||
@Output() bookRetrieved?: EventEmitter<any>;
|
||||
|
||||
loaded: boolean;
|
||||
component = AddonModBookProvider.COMPONENT;
|
||||
componentId: number;
|
||||
chapterContent: string;
|
||||
previousChapter: string;
|
||||
nextChapter: string;
|
||||
|
||||
// Data for context menu.
|
||||
externalUrl: string;
|
||||
description: string;
|
||||
refreshIcon: string;
|
||||
prefetchStatusIcon: string;
|
||||
prefetchText: string;
|
||||
size: string;
|
||||
|
||||
protected chapters: AddonModBookTocChapter[];
|
||||
protected currentChapter: string;
|
||||
protected contentsMap: AddonModBookContentsMap;
|
||||
protected isDestroyed = false;
|
||||
protected statusObserver;
|
||||
|
||||
constructor(private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider,
|
||||
private domUtils: CoreDomUtilsProvider, private appProvider: CoreAppProvider, private textUtils: CoreTextUtilsProvider,
|
||||
private courseHelper: CoreCourseHelperProvider, private prefetchDelegate: AddonModBookPrefetchHandler,
|
||||
private popoverCtrl: PopoverController, private translate: TranslateService, @Optional() private content: Content) {
|
||||
this.bookRetrieved = new EventEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.description = this.module.description;
|
||||
this.componentId = this.module.id;
|
||||
this.externalUrl = this.module.url;
|
||||
this.loaded = false;
|
||||
this.refreshIcon = 'spinner';
|
||||
|
||||
this.fetchContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} [refresher] Refresher.
|
||||
* @param {Function} [done] Function to call when done.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
doRefresh(refresher?: any, done?: () => void): Promise<any> {
|
||||
this.refreshIcon = 'spinner';
|
||||
|
||||
return this.bookProvider.invalidateContent(this.module.id, this.courseId).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.fetchContent(this.currentChapter, true);
|
||||
}).finally(() => {
|
||||
this.refreshIcon = 'refresh';
|
||||
refresher && refresher.complete();
|
||||
done && done();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the TOC.
|
||||
*
|
||||
* @param {MouseEvent} event Event.
|
||||
*/
|
||||
showToc(event: MouseEvent): void {
|
||||
const popover = this.popoverCtrl.create(AddonModBookTocPopoverComponent, {
|
||||
chapters: this.chapters
|
||||
});
|
||||
|
||||
popover.onDidDismiss((chapterId) => {
|
||||
this.changeChapter(chapterId);
|
||||
});
|
||||
|
||||
popover.present({
|
||||
ev: event
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current chapter.
|
||||
*
|
||||
* @param {string} chapterId Chapter to load.
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
changeChapter(chapterId: string): void {
|
||||
if (chapterId && chapterId != this.currentChapter) {
|
||||
this.loaded = false;
|
||||
this.refreshIcon = 'spinner';
|
||||
this.loadChapter(chapterId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the description.
|
||||
*/
|
||||
expandDescription(): void {
|
||||
this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component, this.module.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch the module.
|
||||
*/
|
||||
prefetch(): void {
|
||||
this.courseHelper.contextMenuPrefetch(this, this.module, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm and remove downloaded files.
|
||||
*/
|
||||
removeFiles(): void {
|
||||
this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download book contents and load the current chapter.
|
||||
*
|
||||
* @param {string} [chapterId] Chapter to load.
|
||||
* @param {boolean} [refresh] Whether we're refreshing data.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchContent(chapterId?: string, refresh?: boolean): Promise<any> {
|
||||
const promises = [];
|
||||
let downloadFailed = false;
|
||||
|
||||
// Try to get the book data.
|
||||
promises.push(this.bookProvider.getBook(this.courseId, this.module.id).then((book) => {
|
||||
this.bookRetrieved.emit(book);
|
||||
this.description = book.intro || this.description;
|
||||
}).catch(() => {
|
||||
// Ignore errors since this WS isn't available in some Moodle versions.
|
||||
}));
|
||||
|
||||
// Download content. This function also loads module contents if needed.
|
||||
promises.push(this.prefetchDelegate.download(this.module, this.courseId).catch(() => {
|
||||
// Mark download as failed but go on since the main files could have been downloaded.
|
||||
downloadFailed = true;
|
||||
|
||||
if (!this.module.contents.length) {
|
||||
// Try to load module contents for offline usage.
|
||||
return this.courseProvider.loadModuleContents(this.module, this.courseId);
|
||||
}
|
||||
}));
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
this.contentsMap = this.bookProvider.getContentsMap(this.module.contents);
|
||||
this.chapters = this.bookProvider.getTocList(this.module.contents);
|
||||
|
||||
if (typeof this.currentChapter == 'undefined') {
|
||||
this.currentChapter = this.bookProvider.getFirstChapter(this.chapters);
|
||||
}
|
||||
|
||||
// Show chapter.
|
||||
return this.loadChapter(chapterId || this.currentChapter).then(() => {
|
||||
if (downloadFailed && this.appProvider.isOnline()) {
|
||||
// We could load the main file but the download failed. Show error message.
|
||||
this.domUtils.showErrorModal('core.errordownloadingsomefiles', true);
|
||||
}
|
||||
|
||||
// All data obtained, now fill the context menu.
|
||||
this.courseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component);
|
||||
}).catch(() => {
|
||||
// Ignore errors, they're handled inside the loadChapter function.
|
||||
});
|
||||
}).catch((error) => {
|
||||
// Error getting data, fail.
|
||||
this.loaded = true;
|
||||
this.refreshIcon = 'refresh';
|
||||
this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a book chapter.
|
||||
*
|
||||
* @param {string} chapterId Chapter to load.
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected loadChapter(chapterId: string): Promise<void> {
|
||||
this.currentChapter = chapterId;
|
||||
this.content && this.content.scrollToTop();
|
||||
|
||||
return this.bookProvider.getChapterContent(this.contentsMap, chapterId, this.module.id).then((content) => {
|
||||
this.chapterContent = content;
|
||||
this.previousChapter = this.bookProvider.getPreviousChapter(this.chapters, chapterId);
|
||||
this.nextChapter = this.bookProvider.getNextChapter(this.chapters, chapterId);
|
||||
|
||||
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
|
||||
this.bookProvider.logView(this.module.instance, chapterId).then(() => {
|
||||
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
|
||||
if (!this.nextChapter) {
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_book.errorchapter', true);
|
||||
|
||||
return Promise.reject(null);
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
this.refreshIcon = 'refresh';
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
this.statusObserver && this.statusObserver.off();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<a ion-button icon-only clear *ngIf="previous > 0" (click)="action.emit(previous)" title="{{ 'core.previous' | translate }}">
|
||||
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>
|
||||
</a>
|
||||
</ion-col>
|
||||
<ion-col text-right>
|
||||
<a ion-button icon-only clear *ngIf="next > 0" (click)="action.emit(next)" title="{{ 'core.next' | translate }}">
|
||||
<ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon>
|
||||
</a>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
|
@ -0,0 +1,32 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Component to navigate to previous or next chapter in a book.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-book-navigation-arrows',
|
||||
templateUrl: 'navigation-arrows.html'
|
||||
})
|
||||
export class AddonModBookNavigationArrowsComponent {
|
||||
@Input() previous?: string; // Previous chapter ID.
|
||||
@Input() next?: string; // Next chapter ID.
|
||||
@Output() action?: EventEmitter<string>; // Will emit an event when the item clicked.
|
||||
|
||||
constructor() {
|
||||
this.action = new EventEmitter();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<ion-list>
|
||||
<a ion-item text-wrap *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)" detail-none>
|
||||
<p [attr.padding-left]="chapter.level == 1 ? true : null">{{chapter.title}}</p>
|
||||
</a>
|
||||
</ion-list>
|
|
@ -0,0 +1,41 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 } from '@angular/core';
|
||||
import { NavParams, ViewController } from 'ionic-angular';
|
||||
import { AddonModBookTocChapter } from '../../providers/book';
|
||||
|
||||
/**
|
||||
* Component to display the TOC of a book.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-book-toc-popover',
|
||||
templateUrl: 'toc-popover.html'
|
||||
})
|
||||
export class AddonModBookTocPopoverComponent {
|
||||
chapters: AddonModBookTocChapter[];
|
||||
|
||||
constructor(navParams: NavParams, private viewCtrl: ViewController) {
|
||||
this.chapters = navParams.get('chapters') || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when a course is clicked.
|
||||
*
|
||||
* @param {string} id ID of the clicked chapter.
|
||||
*/
|
||||
loadChapter(id: string): void {
|
||||
this.viewCtrl.dismiss(id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"errorchapter": "Error reading chapter of book."
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="bookComponent.loaded" (ionRefresh)="bookComponent.doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<addon-mod-book-index [module]="module" [courseId]="courseId" (bookRetrieved)="updateData($event)"></addon-mod-book-index>
|
||||
</ion-content>
|
|
@ -0,0 +1,33 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreDirectivesModule } from '../../../../../directives/directives.module';
|
||||
import { AddonModBookComponentsModule } from '../../components/components.module';
|
||||
import { AddonModBookIndexPage } from './index';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModBookIndexPage,
|
||||
],
|
||||
imports: [
|
||||
CoreDirectivesModule,
|
||||
AddonModBookComponentsModule,
|
||||
IonicPageModule.forChild(AddonModBookIndexPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModBookIndexPageModule {}
|
|
@ -0,0 +1,48 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { AddonModBookIndexComponent } from '../../components/index/index';
|
||||
|
||||
/**
|
||||
* Page that displays a book.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-book-index' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-book-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModBookIndexPage {
|
||||
@ViewChild(AddonModBookIndexComponent) bookComponent: AddonModBookIndexComponent;
|
||||
|
||||
title: string;
|
||||
module: any;
|
||||
courseId: number;
|
||||
|
||||
constructor(navParams: NavParams) {
|
||||
this.module = navParams.get('module') || {};
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.title = this.module.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update some data based on the book instance.
|
||||
*
|
||||
* @param {any} book Book instance.
|
||||
*/
|
||||
updateData(book: any): void {
|
||||
this.title = book.name || this.title;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,399 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Http, Response } from '@angular/http';
|
||||
import { CoreFileProvider } from '../../../../providers/file';
|
||||
import { CoreFilepoolProvider } from '../../../../providers/filepool';
|
||||
import { CoreLoggerProvider } from '../../../../providers/logger';
|
||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
|
||||
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
|
||||
import { CoreCourseProvider } from '../../../../core/course/providers/course';
|
||||
|
||||
/**
|
||||
* A book chapter inside the toc list.
|
||||
*/
|
||||
export interface AddonModBookTocChapter {
|
||||
/**
|
||||
* ID to identify the chapter.
|
||||
* @type {string}
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Chapter's title.
|
||||
* @type {string}
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* The chapter's level.
|
||||
* @type {number}
|
||||
*/
|
||||
level: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path
|
||||
* is identified by the relative path in the book, and the value is the URL of the file.
|
||||
*/
|
||||
export type AddonModBookContentsMap = {[chapter: string]: {indexUrl?: string, paths: {[path: string]: string}}};
|
||||
|
||||
/**
|
||||
* Service that provides some features for books.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModBookProvider {
|
||||
static COMPONENT = 'mmaModBook';
|
||||
|
||||
protected ROOT_CACHE_KEY = 'mmaModBook:';
|
||||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
|
||||
private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, private http: Http,
|
||||
private utils: CoreUtilsProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
|
||||
this.logger = logger.getInstance('AddonModBookProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book by course module ID.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {number} cmId Course module ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the book is retrieved.
|
||||
*/
|
||||
getBook(courseId: number, cmId: number, siteId?: string): Promise<any> {
|
||||
return this.getBookByField(courseId, 'coursemodule', cmId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book with key=value. If more than one is found, only the first will be returned.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} key Name of the property to check.
|
||||
* @param {any} value Value to search.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the book is retrieved.
|
||||
*/
|
||||
protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getBookDataCacheKey(courseId)
|
||||
};
|
||||
|
||||
return site.read('mod_book_get_books_by_courses', params, preSets).then((response) => {
|
||||
// Search the book.
|
||||
if (response && response.books) {
|
||||
for (const i in response.books) {
|
||||
const book = response.books[i];
|
||||
if (book[key] == value) {
|
||||
return book;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for get book data WS calls.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getBookDataCacheKey(courseId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'book:' + courseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chapter contents.
|
||||
*
|
||||
* @param {AddonModBookContentsMap} contentsMap Contents map returned by getContentsMap.
|
||||
* @param {string} chapterId Chapter to retrieve.
|
||||
* @param {number} moduleId The module ID.
|
||||
* @return {Promise<string>} Promise resolved with the contents.
|
||||
*/
|
||||
getChapterContent(contentsMap: AddonModBookContentsMap, chapterId: string, moduleId: number): Promise<string> {
|
||||
const indexUrl = contentsMap[chapterId] ? contentsMap[chapterId].indexUrl : undefined,
|
||||
siteId = this.sitesProvider.getCurrentSiteId();
|
||||
let promise;
|
||||
|
||||
if (!indexUrl) {
|
||||
// It shouldn't happen.
|
||||
this.logger.debug('Could not locate the index chapter');
|
||||
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
if (this.fileProvider.isAvailable()) {
|
||||
promise = this.filepoolProvider.downloadUrl(siteId, indexUrl, false, AddonModBookProvider.COMPONENT, moduleId);
|
||||
} else {
|
||||
// We return the live URL.
|
||||
return Promise.resolve(this.sitesProvider.getCurrentSite().fixPluginfileURL(indexUrl));
|
||||
}
|
||||
|
||||
return promise.then((url) => {
|
||||
// Fetch the URL content.
|
||||
const observable = this.http.get(url);
|
||||
|
||||
return this.utils.observableToPromise(observable).then((response: Response): any => {
|
||||
const content = response.text();
|
||||
if (typeof content !== 'string') {
|
||||
return Promise.reject(null);
|
||||
} else {
|
||||
// Now that we have the content, we update the SRC to point back to the external resource.
|
||||
return this.domUtils.restoreSourcesInHtml(content, contentsMap[chapterId].paths);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of book contents into an object where contents are organized in chapters.
|
||||
* Each chapter has an indexUrl and the list of contents in that chapter.
|
||||
*
|
||||
* @param {any[]} contents The module contents.
|
||||
* @return {AddonModBookContentsMap} Contents map.
|
||||
*/
|
||||
getContentsMap(contents: any[]): AddonModBookContentsMap {
|
||||
const map: AddonModBookContentsMap = {};
|
||||
|
||||
if (!contents) {
|
||||
return map;
|
||||
}
|
||||
|
||||
contents.forEach((content) => {
|
||||
if (this.isFileDownloadable(content)) {
|
||||
let chapter,
|
||||
matches,
|
||||
split,
|
||||
filepathIsChapter,
|
||||
key;
|
||||
|
||||
// Search the chapter number in the filepath.
|
||||
matches = content.filepath.match(/\/(\d+)\//);
|
||||
if (matches && matches[1]) {
|
||||
chapter = matches[1];
|
||||
filepathIsChapter = content.filepath == '/' + chapter + '/';
|
||||
|
||||
// Init the chapter if it's not defined yet.
|
||||
map[chapter] = map[chapter] || { paths: {} };
|
||||
|
||||
if (content.filename == 'index.html' && filepathIsChapter) {
|
||||
// Index of the chapter, set indexUrl of the chapter.
|
||||
map[chapter].indexUrl = content.fileurl;
|
||||
} else {
|
||||
if (filepathIsChapter) {
|
||||
// It's a file in the root folder OR the WS isn't returning the filepath as it should (MDL-53671).
|
||||
// Try to get the path to the file from the URL.
|
||||
split = content.fileurl.split('mod_book/chapter' + content.filepath);
|
||||
key = split[1] || content.filename; // Use filename if we couldn't find the path.
|
||||
} else {
|
||||
// Remove the chapter folder from the path and add the filename.
|
||||
key = content.filepath.replace('/' + chapter + '/', '') + content.filename;
|
||||
}
|
||||
|
||||
map[chapter].paths[this.textUtils.decodeURIComponent(key)] = content.fileurl;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first chapter of a book.
|
||||
*
|
||||
* @param {AddonModBookTocChapter[]} chapters The chapters list.
|
||||
* @return {string} The chapter id.
|
||||
*/
|
||||
getFirstChapter(chapters: AddonModBookTocChapter[]): string {
|
||||
if (!chapters || !chapters.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return chapters[0].id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next chapter to the given one.
|
||||
*
|
||||
* @param {AddonModBookTocChapter[]} chapters The chapters list.
|
||||
* @param {string} chapterId The current chapter.
|
||||
* @return {string} The next chapter id.
|
||||
*/
|
||||
getNextChapter(chapters: AddonModBookTocChapter[], chapterId: string): string {
|
||||
let next = '0';
|
||||
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
if (chapters[i].id == chapterId) {
|
||||
if (typeof chapters[i + 1] != 'undefined') {
|
||||
next = chapters[i + 1].id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous chapter to the given one.
|
||||
*
|
||||
* @param {AddonModBookTocChapter[]} chapters The chapters list.
|
||||
* @param {string} chapterId The current chapter.
|
||||
* @return {string} The next chapter id.
|
||||
*/
|
||||
getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: string): string {
|
||||
let previous = '0';
|
||||
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
if (chapters[i].id == chapterId) {
|
||||
break;
|
||||
}
|
||||
previous = chapters[i].id;
|
||||
}
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book toc as an array.
|
||||
*
|
||||
* @param {any[]} contents The module contents.
|
||||
* @return {any[]} The toc.
|
||||
*/
|
||||
getToc(contents: any[]): any[] {
|
||||
if (!contents || !contents.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(contents[0].content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book toc as an array of chapters (not nested).
|
||||
*
|
||||
* @param {any[]} contents The module contents.
|
||||
* @return {AddonModBookTocChapter[]} The toc as a list.
|
||||
*/
|
||||
getTocList(contents: any[]): AddonModBookTocChapter[] {
|
||||
const chapters = [],
|
||||
toc = this.getToc(contents);
|
||||
|
||||
toc.forEach((chapter) => {
|
||||
// Add the chapter to the list.
|
||||
let chapterId = chapter.href.replace('/index.html', '');
|
||||
chapters.push({id: chapterId, title: chapter.title, level: chapter.level});
|
||||
|
||||
if (chapter.subitems) {
|
||||
// Add all the subchapters to the list.
|
||||
chapter.subitems.forEach((subChapter) => {
|
||||
chapterId = subChapter.href.replace('/index.html', '');
|
||||
chapters.push({id: chapterId, title: subChapter.title, level: subChapter.level});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return chapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates book data.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateBookData(courseId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getBookDataCacheKey(courseId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {number} courseId Course ID of the module.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.invalidateBookData(courseId, siteId));
|
||||
promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModBookProvider.COMPONENT, moduleId));
|
||||
promises.push(this.courseProvider.invalidateModule(moduleId, siteId));
|
||||
|
||||
return this.utils.allPromises(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is downloadable. The file param must have a 'type' attribute like in core_course_get_contents response.
|
||||
*
|
||||
* @param {any} file File to check.
|
||||
* @return {boolean} Whether it's downloadable.
|
||||
*/
|
||||
isFileDownloadable(file: any): boolean {
|
||||
return file.type === 'file';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the plugin is enabled.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
||||
*/
|
||||
isPluginEnabled(siteId?: string): Promise<boolean> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.canDownloadFiles();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a book as being viewed.
|
||||
*
|
||||
* @param {number} id Module ID.
|
||||
* @param {string} chapterId Chapter ID.
|
||||
* @return {Promise<any>} Promise resolved when the WS call is successful.
|
||||
*/
|
||||
logView(id: number, chapterId: string): Promise<any> {
|
||||
if (id) {
|
||||
const params = {
|
||||
bookid: id,
|
||||
chapterid: chapterId
|
||||
};
|
||||
|
||||
return this.sitesProvider.getCurrentSite().write('mod_book_view_book', params).then((response) => {
|
||||
if (!response.status) {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreContentLinksModuleIndexHandler } from '../../../../core/contentlinks/classes/module-index-handler';
|
||||
import { CoreCourseHelperProvider } from '../../../../core/course/providers/helper';
|
||||
|
||||
/**
|
||||
* Handler to treat links to book.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModBookLinkHandler extends CoreContentLinksModuleIndexHandler {
|
||||
name = 'AddonModBookLinkHandler';
|
||||
|
||||
constructor(courseHelper: CoreCourseHelperProvider) {
|
||||
super(courseHelper, 'mmaModBook', 'book');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { NavController, NavOptions } from 'ionic-angular';
|
||||
import { AddonModBookProvider } from './book';
|
||||
import { AddonModBookIndexComponent } from '../components/index/index';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '../../../../core/course/providers/module-delegate';
|
||||
import { CoreCourseProvider } from '../../../../core/course/providers/course';
|
||||
|
||||
/**
|
||||
* Handler to support book modules.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModBookModuleHandler implements CoreCourseModuleHandler {
|
||||
name = 'book';
|
||||
|
||||
constructor(protected bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider) { }
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return this.bookProvider.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data required to display the module in the course contents view.
|
||||
*
|
||||
* @param {any} module The module object.
|
||||
* @param {number} courseId The course ID.
|
||||
* @param {number} sectionId The section ID.
|
||||
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||
*/
|
||||
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||
return {
|
||||
icon: this.courseProvider.getModuleIconSrc('book'),
|
||||
title: module.name,
|
||||
class: 'addon-mod_book-handler',
|
||||
showDownloadButton: true,
|
||||
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||
navCtrl.push('AddonModBookIndexPage', {module: module, courseId: courseId}, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||
* The component returned must implement CoreCourseModuleMainComponent.
|
||||
*
|
||||
* @param {any} course The course object.
|
||||
* @param {any} module The module object.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getMainComponent(course: any, module: any): any {
|
||||
return AddonModBookIndexComponent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreCourseModulePrefetchHandlerBase } from '../../../../core/course/classes/module-prefetch-handler';
|
||||
import { AddonModBookProvider } from './book';
|
||||
|
||||
/**
|
||||
* Handler to prefetch books.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModBookPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
|
||||
name = 'book';
|
||||
component = AddonModBookProvider.COMPONENT;
|
||||
updatesNames = /^configuration$|^.*files$|^entries$/;
|
||||
isResource = true;
|
||||
|
||||
constructor(injector: Injector, protected bookProvider: AddonModBookProvider) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download or prefetch the content.
|
||||
*
|
||||
* @param {any} module The module object returned by WS.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {boolean} [prefetch] True to prefetch, false to download right away.
|
||||
* @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files
|
||||
* relative paths and make the package work in an iframe. Undefined to download the files
|
||||
* in the filepool root folder.
|
||||
* @return {Promise<any>} Promise resolved when all content is downloaded. Data returned is not reliable.
|
||||
*/
|
||||
downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(super.downloadOrPrefetch(module, courseId, prefetch));
|
||||
promises.push(this.bookProvider.getBook(courseId, module.id).catch(() => {
|
||||
// Ignore errors since this WS isn't available in some Moodle versions.
|
||||
}));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns module intro files.
|
||||
*
|
||||
* @param {any} module The module object returned by WS.
|
||||
* @param {number} courseId Course ID.
|
||||
* @return {Promise<any[]>} Promise resolved with list of intro files.
|
||||
*/
|
||||
getIntroFiles(module: any, courseId: number): Promise<any[]> {
|
||||
return this.bookProvider.getBook(courseId, module.id).catch(() => {
|
||||
// Not found, return undefined so module description is used.
|
||||
}).then((book) => {
|
||||
return this.getIntroFilesFromInstance(module, book);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||
return this.bookProvider.invalidateContent(moduleId, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate WS calls needed to determine module status.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when invalidated.
|
||||
*/
|
||||
invalidateModule(module: any, courseId: number): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.bookProvider.invalidateBookData(courseId));
|
||||
promises.push(this.courseProvider.invalidateModule(module.id));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return this.bookProvider.isPluginEnabled();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
a.core-course-module-handler.addon-mod-label-handler {
|
||||
align-items: center;
|
||||
}
|
|
@ -57,6 +57,7 @@ export class AddonModLabelModuleHandler implements CoreCourseModuleHandler {
|
|||
|
||||
/**
|
||||
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||
* The component returned must implement CoreCourseModuleMainComponent.
|
||||
*
|
||||
* @param {any} course The course object.
|
||||
* @param {any} module The module object.
|
||||
|
|
|
@ -67,6 +67,7 @@ import { CoreGradesModule } from '../core/grades/grades.module';
|
|||
import { AddonCalendarModule } from '../addon/calendar/calendar.module';
|
||||
import { AddonUserProfileFieldModule } from '../addon/userprofilefield/userprofilefield.module';
|
||||
import { AddonFilesModule } from '../addon/files/files.module';
|
||||
import { AddonModBookModule } from '../addon/mod/book/book.module';
|
||||
import { AddonModLabelModule } from '../addon/mod/label/label.module';
|
||||
|
||||
// For translate loader. AoT requires an exported function for factories.
|
||||
|
@ -107,6 +108,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
|||
AddonCalendarModule,
|
||||
AddonUserProfileFieldModule,
|
||||
AddonFilesModule,
|
||||
AddonModBookModule,
|
||||
AddonModLabelModule
|
||||
],
|
||||
bootstrap: [IonicApp],
|
||||
|
|
|
@ -134,7 +134,7 @@ export class CoreDelegate {
|
|||
if (handler && handler[fnName]) {
|
||||
return handler[fnName].apply(handler, params);
|
||||
} else if (this.defaultHandler && this.defaultHandler[fnName]) {
|
||||
return this.defaultHandler[fnName].apply(this, params);
|
||||
return this.defaultHandler[fnName].apply(this.defaultHandler, params);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,9 +143,9 @@ export class CoreDelegate {
|
|||
*
|
||||
* @param {string} handlerName The handler name.
|
||||
* @param {boolean} [enabled] Only enabled, or any.
|
||||
* @return {any} Handler.
|
||||
* @return {CoreDelegateHandler} Handler.
|
||||
*/
|
||||
protected getHandler(handlerName: string, enabled: boolean = false): any {
|
||||
protected getHandler(handlerName: string, enabled: boolean = false): CoreDelegateHandler {
|
||||
return enabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
|
||||
}
|
||||
|
||||
|
|
|
@ -649,11 +649,11 @@ export class SQLiteDB {
|
|||
*
|
||||
* @param {string} table The database table.
|
||||
* @param {object} data An object with the fields to insert/update: fieldname=>fieldvalue.
|
||||
* @param {object} conditions The conditions to check if the record already exists.
|
||||
* @param {object} conditions The conditions to check if the record already exists (and to update it).
|
||||
* @return {Promise<any>} Promise resolved with done.
|
||||
*/
|
||||
insertOrUpdateRecord(table: string, data: object, conditions: object): Promise<any> {
|
||||
return this.getRecord(table, conditions || data).then(() => {
|
||||
return this.getRecord(table, conditions).then(() => {
|
||||
// It exists, update it.
|
||||
return this.updateRecords(table, data, conditions);
|
||||
}).catch(() => {
|
||||
|
@ -854,7 +854,7 @@ export class SQLiteDB {
|
|||
// Create the list of params using the "data" object and the params for the where clause.
|
||||
params = Object.keys(data).map((key) => data[key]);
|
||||
if (where && whereParams) {
|
||||
params = params.concat(whereParams[1]);
|
||||
params = params.concat(whereParams);
|
||||
}
|
||||
|
||||
return this.execute(sql, params);
|
||||
|
@ -868,7 +868,7 @@ export class SQLiteDB {
|
|||
*/
|
||||
whereClause(conditions: any = {}): any[] {
|
||||
if (!conditions || !Object.keys(conditions).length) {
|
||||
return ['', []];
|
||||
return ['1 = 1', []];
|
||||
}
|
||||
|
||||
const where = [],
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
core-context-menu-popover {
|
||||
.item-md ion-icon[item-start] + .item-inner,
|
||||
.item-md ion-icon[item-start] + .item-input {
|
||||
@include margin-horizontal(5px, null);
|
||||
}
|
||||
}
|
|
@ -12,9 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core';
|
||||
import { PopoverController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreDomUtilsProvider } from '../../providers/utils/dom';
|
||||
import { CoreContextMenuItemComponent } from './context-menu-item';
|
||||
import { CoreContextMenuPopoverComponent } from './context-menu-popover';
|
||||
import { Subject } from 'rxjs';
|
||||
|
@ -26,7 +27,7 @@ import { Subject } from 'rxjs';
|
|||
selector: 'core-context-menu',
|
||||
templateUrl: 'context-menu.html'
|
||||
})
|
||||
export class CoreContextMenuComponent implements OnInit {
|
||||
export class CoreContextMenuComponent implements OnInit, OnDestroy {
|
||||
@Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
|
||||
@Input() title?: string; // Aria label and text to be shown on the top of the popover.
|
||||
|
||||
|
@ -34,8 +35,11 @@ export class CoreContextMenuComponent implements OnInit {
|
|||
ariaLabel: string;
|
||||
protected items: CoreContextMenuItemComponent[] = [];
|
||||
protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
|
||||
protected instanceId: string;
|
||||
protected parentContextMenu: CoreContextMenuComponent;
|
||||
|
||||
constructor(private translate: TranslateService, private popoverCtrl: PopoverController) {
|
||||
constructor(private translate: TranslateService, private popoverCtrl: PopoverController, private elementRef: ElementRef,
|
||||
private domUtils: CoreDomUtilsProvider) {
|
||||
// Create the stream and subscribe to it. We ignore successive changes during 250ms.
|
||||
this.itemsChangedStream = new Subject<void>();
|
||||
this.itemsChangedStream.auditTime(250).subscribe(() => {
|
||||
|
@ -43,7 +47,14 @@ export class CoreContextMenuComponent implements OnInit {
|
|||
this.hideMenu = !this.items.some((item) => {
|
||||
return !item.hidden;
|
||||
});
|
||||
|
||||
// Sort the items by priority.
|
||||
this.items.sort((a, b) => {
|
||||
return a.priority <= b.priority ? 1 : -1;
|
||||
});
|
||||
});
|
||||
|
||||
this.instanceId = this.domUtils.storeInstanceByElement(elementRef.nativeElement, this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,15 +71,44 @@ export class CoreContextMenuComponent implements OnInit {
|
|||
* @param {CoreContextMenuItemComponent} item The item to add.
|
||||
*/
|
||||
addItem(item: CoreContextMenuItemComponent): void {
|
||||
this.items.push(item);
|
||||
this.itemsChanged();
|
||||
if (this.parentContextMenu) {
|
||||
// All items were moved to the "parent" menu. Add the item in there.
|
||||
this.parentContextMenu.addItem(item);
|
||||
} else {
|
||||
this.items.push(item);
|
||||
this.itemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when the items change.
|
||||
*/
|
||||
itemsChanged(): void {
|
||||
this.itemsChangedStream.next();
|
||||
if (this.parentContextMenu) {
|
||||
// All items were moved to the "parent" menu, call the function in there.
|
||||
this.parentContextMenu.itemsChanged();
|
||||
} else {
|
||||
this.itemsChangedStream.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the current context menu with the one passed as parameter. All the items in this menu will be moved to the
|
||||
* one passed as parameter.
|
||||
*
|
||||
* @param {CoreContextMenuComponent} contextMenu The context menu where to move the items.
|
||||
*/
|
||||
mergeContextMenus(contextMenu: CoreContextMenuComponent): void {
|
||||
this.parentContextMenu = contextMenu;
|
||||
|
||||
// Add all the items to the other menu.
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
contextMenu.addItem(this.items[i]);
|
||||
}
|
||||
|
||||
// Remove all items from the current menu.
|
||||
this.items = [];
|
||||
this.itemsChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,11 +117,16 @@ export class CoreContextMenuComponent implements OnInit {
|
|||
* @param {CoreContextMenuItemComponent} item The item to remove.
|
||||
*/
|
||||
removeItem(item: CoreContextMenuItemComponent): void {
|
||||
const index = this.items.indexOf(item);
|
||||
if (index >= 0) {
|
||||
this.items.splice(index, 1);
|
||||
if (this.parentContextMenu) {
|
||||
// All items were moved to the "parent" menu. Remove the item from there.
|
||||
this.parentContextMenu.removeItem(item);
|
||||
} else {
|
||||
const index = this.items.indexOf(item);
|
||||
if (index >= 0) {
|
||||
this.items.splice(index, 1);
|
||||
}
|
||||
this.itemsChanged();
|
||||
}
|
||||
this.itemsChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,4 +140,11 @@ export class CoreContextMenuComponent implements OnInit {
|
|||
ev: event
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.domUtils.removeInstanceById(this.instanceId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,6 +99,19 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a certain function on the component.
|
||||
*
|
||||
* @param {string} name Name of the function to call.
|
||||
* @param {any[]} params List of params to send to the function.
|
||||
* @return {any} Result of the call. Undefined if no component instance or the function doesn't exist.
|
||||
*/
|
||||
callComponentFunction(name: string, params: any[]): any {
|
||||
if (this.instance && typeof this.instance[name] == 'function') {
|
||||
return this.instance[name].apply(this.instance, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a component, add it to a container and set the input data.
|
||||
*
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import { Component, Input, OnInit, ContentChildren, ElementRef, QueryList } from '@angular/core';
|
||||
import { Button } from 'ionic-angular';
|
||||
import { CoreLoggerProvider } from '../../providers/logger';
|
||||
import { CoreDomUtilsProvider } from '../../providers/utils/dom';
|
||||
|
||||
/**
|
||||
|
@ -63,41 +64,80 @@ export class CoreNavBarButtonsComponent implements OnInit {
|
|||
protected element: HTMLElement;
|
||||
protected _buttons: QueryList<Button>;
|
||||
protected _hidden: boolean;
|
||||
protected logger: any;
|
||||
|
||||
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider) {
|
||||
constructor(element: ElementRef, logger: CoreLoggerProvider, private domUtils: CoreDomUtilsProvider) {
|
||||
this.element = element.nativeElement;
|
||||
this.logger = logger.getInstance('CoreNavBarButtonsComponent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const header = this.searchHeader();
|
||||
this.searchHeader().then((header) => {
|
||||
if (header) {
|
||||
// Search the right buttons container (start, end or any).
|
||||
let selector = 'ion-buttons',
|
||||
buttonsContainer: HTMLElement;
|
||||
|
||||
if (header) {
|
||||
// Search the right buttons container (start, end or any).
|
||||
let selector = 'ion-buttons',
|
||||
buttonsContainer: HTMLElement;
|
||||
if (this.element.hasAttribute('start')) {
|
||||
selector += '[start]';
|
||||
} else if (this.element.hasAttribute('end')) {
|
||||
selector += '[end]';
|
||||
}
|
||||
|
||||
if (this.element.hasAttribute('start')) {
|
||||
selector += '[start]';
|
||||
} else if (this.element.hasAttribute('end')) {
|
||||
selector += '[end]';
|
||||
buttonsContainer = <HTMLElement> header.querySelector(selector);
|
||||
if (buttonsContainer) {
|
||||
this.mergeContextMenus(buttonsContainer);
|
||||
|
||||
this.domUtils.moveChildren(this.element, buttonsContainer);
|
||||
} else {
|
||||
this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector);
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
// Header not found.
|
||||
this.logger.warn('Header not found.');
|
||||
});
|
||||
}
|
||||
|
||||
buttonsContainer = <HTMLElement> header.querySelector(selector);
|
||||
if (buttonsContainer) {
|
||||
this.domUtils.moveChildren(this.element, buttonsContainer);
|
||||
}
|
||||
/**
|
||||
* If both button containers have a context menu, merge them into a single one.
|
||||
*
|
||||
* @param {HTMLElement} buttonsContainer The container where the buttons will be moved.
|
||||
*/
|
||||
protected mergeContextMenus(buttonsContainer: HTMLElement): void {
|
||||
// Check if both button containers have a context menu.
|
||||
const mainContextMenu = buttonsContainer.querySelector('core-context-menu');
|
||||
if (!mainContextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secondaryContextMenu = this.element.querySelector('core-context-menu');
|
||||
if (!secondaryContextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Both containers have a context menu. Merge them to prevent having 2 menus at the same time.
|
||||
const mainContextMenuInstance = this.domUtils.getInstanceByElement(mainContextMenu),
|
||||
secondaryContextMenuInstance = this.domUtils.getInstanceByElement(secondaryContextMenu);
|
||||
|
||||
if (mainContextMenuInstance && secondaryContextMenuInstance) {
|
||||
secondaryContextMenuInstance.mergeContextMenus(mainContextMenuInstance);
|
||||
|
||||
// Remove the empty context menu from the DOM.
|
||||
secondaryContextMenu.parentElement.removeChild(secondaryContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the ion-header where the buttons should be added.
|
||||
*
|
||||
* @return {HTMLElement} Header element.
|
||||
* @param {number} [retries] Number of retries so far.
|
||||
* @return {Promise<HTMLElement>} Promise resolved with the header element.
|
||||
*/
|
||||
protected searchHeader(): HTMLElement {
|
||||
protected searchHeader(retries: number = 0): Promise<HTMLElement> {
|
||||
let parentPage: HTMLElement = this.element;
|
||||
|
||||
while (parentPage) {
|
||||
|
@ -112,10 +152,24 @@ export class CoreNavBarButtonsComponent implements OnInit {
|
|||
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||
const header = this.searchHeaderInPage(parentPage);
|
||||
if (header) {
|
||||
return header;
|
||||
return Promise.resolve(header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header not found.
|
||||
if (retries < 5) {
|
||||
// If the component or any of its parent is inside a ng-content or similar it can be detached when it's initialized.
|
||||
// Try again after a while.
|
||||
return new Promise((resolve, reject): void => {
|
||||
setTimeout(() => {
|
||||
this.searchHeader(retries + 1).then(resolve, reject);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
// We've waited enough time, reject.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -301,7 +301,7 @@ export class CoreContentLinksDelegate {
|
|||
|
||||
// Sort by priority.
|
||||
actions = actions.sort((a, b) => {
|
||||
return a.priority >= b.priority ? 1 : -1;
|
||||
return a.priority <= b.priority ? 1 : -1;
|
||||
});
|
||||
|
||||
// Fill result array.
|
||||
|
|
|
@ -21,6 +21,7 @@ import { CoreInitDelegate } from '../../../providers/init';
|
|||
import { CoreLoggerProvider } from '../../../providers/logger';
|
||||
import { CoreSitesProvider } from '../../../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '../../../providers/utils/text';
|
||||
import { CoreUrlUtilsProvider } from '../../../providers/utils/url';
|
||||
import { CoreLoginHelperProvider } from '../../login/providers/helper';
|
||||
import { CoreContentLinksDelegate, CoreContentLinksAction } from './delegate';
|
||||
|
@ -37,7 +38,7 @@ export class CoreContentLinksHelperProvider {
|
|||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider,
|
||||
private contentLinksDelegate: CoreContentLinksDelegate, private appProvider: CoreAppProvider,
|
||||
private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private translate: TranslateService,
|
||||
private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider) {
|
||||
private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider) {
|
||||
this.logger = logger.getInstance('CoreContentLinksHelperProvider');
|
||||
|
||||
// Listen for app launched URLs. If we receive one, check if it's a content link.
|
||||
|
@ -103,7 +104,7 @@ export class CoreContentLinksHelperProvider {
|
|||
const modal = this.domUtils.showModalLoading();
|
||||
let username;
|
||||
|
||||
url = decodeURIComponent(url);
|
||||
url = this.textUtils.decodeURIComponent(url);
|
||||
|
||||
// App opened using custom URL scheme.
|
||||
this.logger.debug('Treating custom URL scheme: ' + url);
|
||||
|
|
|
@ -53,17 +53,11 @@ export type prefetchFunction = (module: any, courseId: number, single: boolean,
|
|||
* recommended to call the prefetchPackage function since it'll handle changing the status of the module.
|
||||
*/
|
||||
export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePrefetchHandler {
|
||||
/**
|
||||
* A name to identify the addon.
|
||||
* @type {string}
|
||||
*/
|
||||
name = 'CoreCourseModulePrefetchHandlerBase';
|
||||
|
||||
/**
|
||||
* Name of the module. It should match the "modname" of the module returned in core_course_get_contents.
|
||||
* @type {string}
|
||||
*/
|
||||
modname = '';
|
||||
name = '';
|
||||
|
||||
/**
|
||||
* The handler's component.
|
||||
|
@ -235,7 +229,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref
|
|||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {number|Promise<number>} Size, or promise resolved with the size.
|
||||
*/
|
||||
getDownloadedSize?(module: any, courseId: number): number | Promise<number> {
|
||||
getDownloadedSize(module: any, courseId: number): number | Promise<number> {
|
||||
const siteId = this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.filepoolProvider.getFilesSizeByComponent(siteId, this.component, module.id);
|
||||
|
@ -324,9 +318,10 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref
|
|||
* Invalidate the prefetched content.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {number} courseId The course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateContent(moduleId: number): Promise<any> {
|
||||
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||
const promises = [],
|
||||
siteId = this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<core-loading [hideUntil]="loaded">
|
||||
<!-- Section selector. -->
|
||||
<core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
|
||||
<div *ngIf="displaySectionSelector && sections && sections.length" no-padding class="clearfix">
|
||||
<div text-wrap *ngIf="displaySectionSelector && sections && sections.length" no-padding class="clearfix">
|
||||
<!-- @todo: How to display availabilityinfo and not visible messages? -->
|
||||
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover">
|
||||
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
|
||||
|
@ -56,14 +56,14 @@
|
|||
</ion-item>
|
||||
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [courseId]="course.id" (completionChanged)="completionChanged.emit()"></core-course-module>
|
||||
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [courseId]="course.id" [downloadEnabled]="downloadEnabled" (completionChanged)="completionChanged.emit()"></core-course-module>
|
||||
</ng-container>
|
||||
</section>
|
||||
</ng-template>
|
||||
|
||||
<!-- Template to render a section download button/progress. -->
|
||||
<ng-template #sectionDownloadTemplate let-section="section">
|
||||
<div *ngIf="section && downloadEnabled" float-end>
|
||||
<div *ngIf="section && downloadEnabled" class="core-button-spinner" float-end>
|
||||
<!-- Download button. -->
|
||||
<button *ngIf="section.showDownload && !section.isDownloading && !section.isCalculating" (click)="prefetch($event, section)" ion-button icon-only clear color="dark" [attr.aria-label]="'core.download' | translate">
|
||||
<ion-icon name="cloud-download"></ion-icon>
|
||||
|
@ -72,9 +72,9 @@
|
|||
<button *ngIf="section.showRefresh && !section.isDownloading && !section.isCalculating" (click)="prefetch($event, section)" ion-button icon-only clear color="dark" [attr.aria-label]="'core.refresh' | translate">
|
||||
<ion-icon name="refresh"></ion-icon>
|
||||
</button>
|
||||
<!-- Spinner (downloading or calculating status). -->
|
||||
<ion-spinner *ngIf="(section.isDownloading && section.total > 0) || section.isCalculating"></ion-spinner>
|
||||
<!-- Download progress. -->
|
||||
<ion-badge class="core-course-download-section-progress" *ngIf="section.isDownloading && section.total > 0 && section.count < section.total">{{section.count}} / {{section.total}}</ion-badge>
|
||||
<!-- Spinner (downloading or calculating status). -->
|
||||
<ion-spinner *ngIf="(section.isDownloading && section.total > 0) || section.isCalculating"></ion-spinner>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
ion-badge.core-course-download-section-progress {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-top: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
|
@ -12,7 +12,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter } from '@angular/core';
|
||||
import {
|
||||
Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList
|
||||
} from '@angular/core';
|
||||
import { Content } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '../../../../providers/events';
|
||||
|
@ -22,6 +24,7 @@ import { CoreCourseProvider } from '../../../course/providers/course';
|
|||
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
|
||||
import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate';
|
||||
import { CoreDynamicComponent } from '../../../../components/dynamic-component/dynamic-component';
|
||||
|
||||
/**
|
||||
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||
|
@ -46,6 +49,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
|
||||
|
||||
@ViewChildren(CoreDynamicComponent) dynamicComponents: QueryList<CoreDynamicComponent>;
|
||||
|
||||
// All the possible component classes.
|
||||
courseFormatComponent: any;
|
||||
courseSummaryComponent: any;
|
||||
|
@ -286,6 +291,23 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} [refresher] Refresher.
|
||||
* @param {Function} [done] Function to call when done.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
doRefresh(refresher?: any, done?: () => void): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
this.dynamicComponents.forEach((component) => {
|
||||
promises.push(Promise.resolve(component.callComponentFunction('doRefresh', [refresher, done])));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
|
|
|
@ -1,22 +1,39 @@
|
|||
<a *ngIf="module && module.visibleoncoursepage !== 0" ion-item text-wrap id="core-course-module-{{module.id}}" class="core-course-module-handler {{module.handlerData.class}}" (click)="moduleClicked($event)" [ngClass]="{'item-media': module.handlerData.icon, 'core-not-clickable': !module.handlerData.action || !module.uservisible === false, 'item-dimmed': module.visible === 0 || module.uservisible === false}" title="{{ module.handlerData.title }}" detail-none>
|
||||
|
||||
<img item-start *ngIf="module.handlerData.icon" [src]="module.handlerData.icon" alt="" role="presentation" class="core-module-icon">
|
||||
<div class="core-module-title">
|
||||
<core-format-text [text]="module.handlerData.title"></core-format-text>
|
||||
|
||||
<core-format-text [text]="module.handlerData.title"></core-format-text>
|
||||
<!-- Buttons. -->
|
||||
<div item-end *ngIf="module.uservisible !== false" class="buttons core-module-buttons" [ngClass]="{'core-button-completion': module.completionstatus}">
|
||||
<!-- Module completion. -->
|
||||
<core-course-module-completion *ngIf="module.completionstatus" [completion]="module.completionstatus" [moduleName]="module.name" (completionChanged)="completionChanged.emit()"></core-course-module-completion>
|
||||
|
||||
<div float-end *ngIf="module.uservisible !== false && ((module.handlerData.buttons && module.handlerData.buttons.length > 0) || spinner || module.completionstatus)" class="buttons core-module-buttons" [ngClass]="{'core-button-completion': module.completionstatus}">
|
||||
<core-course-module-completion *ngIf="module.completionstatus" [completion]="module.completionstatus" [moduleName]="module.name" (completionChanged)="completionChanged.emit()"></core-course-module-completion>
|
||||
<div class="core-module-buttons-more">
|
||||
<!-- Download button. -->
|
||||
<button *ngIf="downloadEnabled && showDownload" [hidden]="spinner || module.handlerData.spinner" ion-button icon-only clear (click)="download($event, false)" color="dark" class="core-animate-show-hide" [attr.aria-label]="'core.download' | translate">
|
||||
<ion-icon name="cloud-download"></ion-icon>
|
||||
</button>
|
||||
|
||||
<button ion-button icon-only clear *ngFor="let button of module.handlerData.buttons" [hidden]="button.hidden" (click)="buttonClicked($event, button)" color="dark" class="core-animate-show-hide" [attr.aria-label]="button.label | translate">
|
||||
<ion-icon [name]="button.icon" [ios]="button.iosIcon || ''" [md]="button.mdIcon || ''"></ion-icon>
|
||||
</button>
|
||||
<!-- Refresh button. -->
|
||||
<button *ngIf="downloadEnabled && showRefresh" [hidden]="spinner || module.handlerData.spinner" ion-button icon-only clear (click)="download($event, true)" color="dark" class="core-animate-show-hide" [attr.aria-label]="'core.refresh' | translate">
|
||||
<ion-icon name="refresh"></ion-icon>
|
||||
</button>
|
||||
|
||||
<ion-spinner *ngIf="module.handlerData.spinner" class="core-animate-show-hide"></ion-spinner>
|
||||
<!-- Buttons defined by the module handler. -->
|
||||
<button ion-button icon-only clear *ngFor="let button of module.handlerData.buttons" [hidden]="button.hidden || spinner || module.handlerData.spinner" (click)="buttonClicked($event, button)" color="dark" class="core-animate-show-hide" [attr.aria-label]="button.label | translate">
|
||||
<ion-icon [name]="button.icon" [ios]="button.iosIcon || ''" [md]="button.mdIcon || ''"></ion-icon>
|
||||
</button>
|
||||
|
||||
<!-- Spinner. -->
|
||||
<ion-spinner *ngIf="spinner || module.handlerData.spinner" class="core-animate-show-hide"></ion-spinner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="module.visible === 0 || module.availabilityinfo">
|
||||
<ion-badge item-end *ngIf="module.visible === 0">{{ 'core.course.hiddenfromstudents' | translate }}</ion-badge>
|
||||
<ion-badge item-end *ngIf="module.availabilityinfo"><core-format-text [text]="module.availabilityinfo"></core-format-text></ion-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="module.visible === 0 || module.availabilityinfo">
|
||||
<ion-badge item-end *ngIf="module.visible === 0">{{ 'core.course.hiddenfromstudents' | translate }}</ion-badge>
|
||||
<ion-badge item-end *ngIf="module.availabilityinfo"><core-format-text [text]="module.availabilityinfo"></core-format-text></ion-badge>
|
||||
</div>
|
||||
<core-format-text *ngIf="module.description" maxHeight="80" [text]="module.description"></core-format-text>
|
||||
<core-format-text class="core-module-description" *ngIf="module.description" maxHeight="80" [text]="module.description"></core-format-text>
|
||||
</a>
|
|
@ -2,29 +2,116 @@ core-course-module {
|
|||
|
||||
a.core-course-module-handler {
|
||||
align-items: flex-start;
|
||||
item-inner {
|
||||
min-height: 52px;
|
||||
|
||||
&.item .item-inner {
|
||||
padding-right: 0;
|
||||
}
|
||||
.label {
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.core-module-icon {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.core-module-icon {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.core-module-buttons {
|
||||
.core-module-title {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 4px;
|
||||
align-items: flex-start;
|
||||
|
||||
.spinner {
|
||||
right: 7px;
|
||||
core-format-text {
|
||||
flex-grow: 2;
|
||||
}
|
||||
.core-module-buttons {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.core-module-buttons,
|
||||
.core-module-buttons-more {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.core-module-buttons core-course-module-completion,
|
||||
.core-module-buttons-more button {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.core-module-buttons-more .spinner {
|
||||
right: 13px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.md core-course-module {
|
||||
.core-module-description,
|
||||
.core-module-description .core-show-more {
|
||||
padding-right: $label-md-margin-end;
|
||||
}
|
||||
|
||||
a.core-course-module-handler .core-module-icon {
|
||||
margin-top: $label-md-margin-top;
|
||||
margin-bottom: $label-md-margin-bottom;
|
||||
}
|
||||
|
||||
.core-module-title core-format-text {
|
||||
padding-top: $label-md-margin-top + 3;
|
||||
}
|
||||
.button-md {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.core-module-buttons-more {
|
||||
min-height: 52px;
|
||||
min-width: 53px;
|
||||
}
|
||||
}
|
||||
|
||||
.ios core-course-module {
|
||||
.core-module-description,
|
||||
.core-module-description .core-show-more {
|
||||
padding-right: $label-ios-margin-end;
|
||||
}
|
||||
|
||||
a.core-course-module-handler .core-module-icon {
|
||||
margin-top: $label-ios-margin-top;
|
||||
margin-bottom: $label-ios-margin-bottom;
|
||||
}
|
||||
|
||||
.core-module-title core-format-text {
|
||||
padding-top: $label-ios-margin-top + 3;
|
||||
}
|
||||
|
||||
.core-module-buttons-more {
|
||||
min-height: 53px;
|
||||
min-width: 58px;
|
||||
}
|
||||
}
|
||||
|
||||
.wp core-course-module {
|
||||
.core-module-description,
|
||||
.core-module-description .core-show-more {
|
||||
padding-right: ($item-wp-padding-end / 2);
|
||||
}
|
||||
|
||||
a.core-course-module-handler .core-module-icon {
|
||||
margin-top: $item-wp-padding-top;
|
||||
margin-bottom: $item-wp-padding-bottom;
|
||||
}
|
||||
|
||||
.core-module-title core-format-text {
|
||||
padding-top: $item-wp-padding-top + 3;
|
||||
}
|
||||
|
||||
.button-wp {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
|
@ -12,9 +12,15 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
|
||||
import { NavController } from 'ionic-angular';
|
||||
import { CoreEventsProvider } from '../../../../providers/events';
|
||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||
import { CoreCourseHelperProvider } from '../../providers/helper';
|
||||
import { CoreCourseModuleHandlerButton } from '../../providers/module-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '../../providers/module-prefetch-delegate';
|
||||
import { CoreConstants } from '../../../constants';
|
||||
|
||||
/**
|
||||
* Component to display a module entry in a list of modules.
|
||||
|
@ -27,12 +33,43 @@ import { CoreCourseModuleHandlerButton } from '../../providers/module-delegate';
|
|||
selector: 'core-course-module',
|
||||
templateUrl: 'module.html'
|
||||
})
|
||||
export class CoreCourseModuleComponent implements OnInit {
|
||||
export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
||||
@Input() module: any; // The module to render.
|
||||
@Input() courseId: number; // The course the module belongs to.
|
||||
@Input('downloadEnabled') set enabled(value: boolean) {
|
||||
this.downloadEnabled = value;
|
||||
|
||||
if (this.module.handlerData.showDownloadButton && this.downloadEnabled && !this.statusObserver) {
|
||||
// First time that the download is enabled. Initialize the data.
|
||||
this.spinner = true; // Show spinner while calculating the status.
|
||||
|
||||
this.prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(this.module);
|
||||
|
||||
// Get current status to decide which icon should be shown.
|
||||
this.prefetchDelegate.getModuleStatus(this.module, this.courseId).then(this.showStatus.bind(this));
|
||||
|
||||
// Listen for changes on this module status.
|
||||
this.statusObserver = this.eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => {
|
||||
if (data.componentId === this.module.id && this.prefetchHandler &&
|
||||
data.component === this.prefetchHandler.component) {
|
||||
this.showStatus(data.status);
|
||||
}
|
||||
}, this.sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
}
|
||||
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when the module completion changes.
|
||||
|
||||
constructor(private navCtrl: NavController) {
|
||||
showDownload: boolean; // Whether to display the download button.
|
||||
showRefresh: boolean; // Whether to display the refresh button.
|
||||
spinner: boolean; // Whether to display a spinner.
|
||||
downloadEnabled: boolean; // Whether the download of sections and modules is enabled.
|
||||
|
||||
protected prefetchHandler: CoreCourseModulePrefetchHandler;
|
||||
protected statusObserver;
|
||||
|
||||
constructor(protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate,
|
||||
protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider,
|
||||
protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider) {
|
||||
this.completionChanged = new EventEmitter();
|
||||
}
|
||||
|
||||
|
@ -68,4 +105,55 @@ export class CoreCourseModuleComponent implements OnInit {
|
|||
button.action(event, this.navCtrl, this.module, this.courseId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the module.
|
||||
*
|
||||
* @param {Event} event Click event.
|
||||
* @param {boolean} refresh Whether it's refreshing.
|
||||
*/
|
||||
download(event: Event, refresh: boolean): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!this.prefetchHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show spinner since this operation might take a while.
|
||||
this.spinner = true;
|
||||
|
||||
// Get download size to ask for confirm if it's high.
|
||||
this.prefetchHandler.getDownloadSize(module, this.courseId).then((size) => {
|
||||
this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh).catch((error) => {
|
||||
// Error or cancelled.
|
||||
this.spinner = false;
|
||||
});
|
||||
}).catch((error) => {
|
||||
// Error getting download size, hide spinner.
|
||||
this.spinner = false;
|
||||
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show download buttons according to module status.
|
||||
*
|
||||
* @param {string} status Module status.
|
||||
*/
|
||||
protected showStatus(status: string): void {
|
||||
if (status) {
|
||||
this.spinner = status === CoreConstants.DOWNLOADING;
|
||||
this.showDownload = status === CoreConstants.NOT_DOWNLOADED;
|
||||
this.showRefresh = status === CoreConstants.OUTDATED ||
|
||||
(!this.prefetchDelegate.canCheckUpdates() && status === CoreConstants.DOWNLOADED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.statusObserver && this.statusObserver.off();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
|
||||
import { Component, Input, OnChanges, SimpleChange, ViewChild } from '@angular/core';
|
||||
import { CoreCourseModuleDelegate } from '../../../providers/module-delegate';
|
||||
import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module';
|
||||
import { CoreDynamicComponent } from '../../../../../components/dynamic-component/dynamic-component';
|
||||
|
||||
/**
|
||||
* Component to display single activity format. It will determine the right component to use and instantiate it.
|
||||
|
@ -30,6 +31,8 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
|||
@Input() sections: any[]; // List of course sections.
|
||||
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||
|
||||
@ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent;
|
||||
|
||||
componentClass: any; // The class of the component to render.
|
||||
data: any = {}; // Data to pass to the component.
|
||||
|
||||
|
@ -52,4 +55,15 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
|||
this.data.module = module;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} [refresher] Refresher.
|
||||
* @param {Function} [done] Function to call when done.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
doRefresh(refresher?: any, done?: () => void): Promise<any> {
|
||||
return Promise.resolve(this.dynamicComponent.callComponentFunction('doRefresh', [refresher, done]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,16 @@ export class CoreCourseFormatSingleActivityHandler implements CoreCourseFormatHa
|
|||
return course.fullname || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the option to enable section/module download should be displayed. Defaults to true.
|
||||
*
|
||||
* @param {any} course The course to check.
|
||||
* @return {boolean} Whether the option to enable section/module download should be displayed
|
||||
*/
|
||||
displayEnableDownload(course: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
*
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
<ion-buttons end>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item [priority]="900" [content]="'core.settings.enabledownloadsection' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
|
||||
<core-context-menu-item [priority]="850" [content]="'core.course.downloadcourse' | translate" (action)="prefetchCourse()" [iconAction]="prefetchCourseData.prefetchCourseIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="displayEnableDownload" [priority]="2000" [content]="'core.settings.enabledownloadsection' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
|
||||
<core-context-menu-item [priority]="1900" [content]="'core.course.downloadcourse' | translate" (action)="prefetchCourse()" [iconAction]="prefetchCourseData.prefetchCourseIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
|
|
|
@ -24,6 +24,7 @@ import { CoreCourseHelperProvider } from '../../providers/helper';
|
|||
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '../../providers/module-prefetch-delegate';
|
||||
import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from '../../providers/options-delegate';
|
||||
import { CoreCourseFormatComponent } from '../../components/format/format';
|
||||
import { CoreCoursesProvider } from '../../../courses/providers/courses';
|
||||
|
||||
/**
|
||||
|
@ -36,6 +37,7 @@ import { CoreCoursesProvider } from '../../../courses/providers/courses';
|
|||
})
|
||||
export class CoreCourseSectionPage implements OnDestroy {
|
||||
@ViewChild(Content) content: Content;
|
||||
@ViewChild(CoreCourseFormatComponent) formatComponent: CoreCourseFormatComponent;
|
||||
|
||||
title: string;
|
||||
course: any;
|
||||
|
@ -51,6 +53,7 @@ export class CoreCourseSectionPage implements OnDestroy {
|
|||
prefetchCourseIcon: 'spinner'
|
||||
};
|
||||
moduleId: number;
|
||||
displayEnableDownload: boolean;
|
||||
|
||||
protected module: any;
|
||||
protected completionObserver;
|
||||
|
@ -71,6 +74,7 @@ export class CoreCourseSectionPage implements OnDestroy {
|
|||
|
||||
// Get the title to display. We dont't have sections yet.
|
||||
this.title = courseFormatDelegate.getCourseTitle(this.course);
|
||||
this.displayEnableDownload = courseFormatDelegate.displayEnableDownload(this.course);
|
||||
|
||||
this.completionObserver = eventsProvider.on(CoreEventsProvider.COMPLETION_MODULE_VIEWED, (data) => {
|
||||
if (data && data.courseId == this.course.id) {
|
||||
|
@ -150,7 +154,19 @@ export class CoreCourseSectionPage implements OnDestroy {
|
|||
|
||||
promises.push(promise.then((completionStatus) => {
|
||||
// Get all the sections.
|
||||
promises.push(this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
|
||||
return this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
|
||||
if (refresh) {
|
||||
// Invalidate the recently downloaded module list. To ensure info can be prefetched.
|
||||
const modules = this.courseProvider.getSectionsModules(sections);
|
||||
|
||||
return this.prefetchDelegate.invalidateModules(modules, this.course.id).then(() => {
|
||||
return sections;
|
||||
});
|
||||
} else {
|
||||
return sections;
|
||||
}
|
||||
}).then((sections) => {
|
||||
|
||||
this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus);
|
||||
|
||||
// Format the name of each section and check if it has content.
|
||||
|
@ -173,7 +189,7 @@ export class CoreCourseSectionPage implements OnDestroy {
|
|||
|
||||
// Get the title again now that we have sections.
|
||||
this.title = this.courseFormatDelegate.getCourseTitle(this.course, this.sections);
|
||||
}));
|
||||
});
|
||||
}));
|
||||
|
||||
// Load the course handlers.
|
||||
|
@ -195,7 +211,9 @@ export class CoreCourseSectionPage implements OnDestroy {
|
|||
doRefresh(refresher: any): void {
|
||||
this.invalidateData().finally(() => {
|
||||
this.loadData(true).finally(() => {
|
||||
refresher.complete();
|
||||
this.formatComponent.doRefresh(refresher).finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -56,6 +56,16 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the option to enable section/module download should be displayed. Defaults to true.
|
||||
*
|
||||
* @param {any} course The course to check.
|
||||
* @return {boolean} Whether the option to enable section/module download should be displayed
|
||||
*/
|
||||
displayEnableDownload(course: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
*
|
||||
|
|
|
@ -43,6 +43,14 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
|
|||
*/
|
||||
canViewAllSections?(course: any): boolean;
|
||||
|
||||
/**
|
||||
* Whether the option to enable section/module download should be displayed. Defaults to true.
|
||||
*
|
||||
* @param {any} course The course to check.
|
||||
* @type {boolean} Whether the option to enable section/module download should be displayed.
|
||||
*/
|
||||
displayEnableDownload?(course: any): boolean;
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
*
|
||||
|
@ -150,6 +158,16 @@ export class CoreCourseFormatDelegate extends CoreDelegate {
|
|||
return this.executeFunction(course.format, 'canViewAllSections', [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the option to enable section/module download should be displayed. Defaults to true.
|
||||
*
|
||||
* @param {any} course The course to check.
|
||||
* @return {boolean} Whether the option to enable section/module download should be displayed
|
||||
*/
|
||||
displayEnableDownload(course: any): boolean {
|
||||
return this.executeFunction(course.format, 'displayEnableDownload', [course]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
*
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '../../../providers/events';
|
||||
import { CoreFilepoolProvider } from '../../../providers/filepool';
|
||||
import { CoreSitesProvider } from '../../../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
|
||||
|
@ -114,7 +115,8 @@ export class CoreCourseHelperProvider {
|
|||
private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider,
|
||||
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
|
||||
private utils: CoreUtilsProvider, private translate: TranslateService, private loginHelper: CoreLoginHelperProvider,
|
||||
private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider) { }
|
||||
private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider,
|
||||
private eventsProvider: CoreEventsProvider) { }
|
||||
|
||||
/**
|
||||
* This function treats every module on the sections provided to load the handler data, treat completion
|
||||
|
@ -358,8 +360,12 @@ export class CoreCourseHelperProvider {
|
|||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
confirmAndRemoveFiles(module: any, courseId: number): Promise<any> {
|
||||
return this.domUtils.showConfirm(this.translate.instant('course.confirmdeletemodulefiles')).then(() => {
|
||||
return this.domUtils.showConfirm(this.translate.instant('core.course.confirmdeletemodulefiles')).then(() => {
|
||||
return this.prefetchDelegate.removeModuleFiles(module, courseId);
|
||||
}).catch((error) => {
|
||||
if (error) {
|
||||
this.domUtils.showErrorModal(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -405,6 +411,39 @@ export class CoreCourseHelperProvider {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to prefetch a module, showing a confirmation modal if the size is big.
|
||||
* This function is meant to be called from a context menu option. It will also modify some data like the prefetch icon.
|
||||
*
|
||||
* @param {any} instance The component instance that has the context menu. It should have prefetchStatusIcon and isDestroyed.
|
||||
* @param {any} module Module to be prefetched
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
contextMenuPrefetch(instance: any, module: any, courseId: number): Promise<any> {
|
||||
const initialIcon = instance.prefetchStatusIcon;
|
||||
let cancelled = false;
|
||||
|
||||
instance.prefetchStatusIcon = 'spinner'; // Show spinner since this operation might take a while.
|
||||
|
||||
// We need to call getDownloadSize, the package might have been updated.
|
||||
return this.prefetchDelegate.getModuleDownloadSize(module, courseId, true).then((size) => {
|
||||
return this.domUtils.confirmDownloadSize(size).catch(() => {
|
||||
// User hasn't confirmed, stop.
|
||||
cancelled = true;
|
||||
|
||||
return Promise.reject(null);
|
||||
}).then(() => {
|
||||
return this.prefetchDelegate.prefetchModule(module, courseId, true);
|
||||
});
|
||||
}).catch((error) => {
|
||||
instance.prefetchStatusIcon = initialIcon;
|
||||
if (!instance.isDestroyed && !cancelled) {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the status of a list of courses.
|
||||
*
|
||||
|
@ -431,6 +470,41 @@ export class CoreCourseHelperProvider {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the Context Menu for a certain module.
|
||||
*
|
||||
* @param {any} instance The component instance that has the context menu.
|
||||
* @param {any} module Module to be prefetched
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} [invalidateCache] Invalidates the cache first.
|
||||
* @param {string} [component] Component of the module.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
fillContextMenu(instance: any, module: any, courseId: number, invalidateCache?: boolean, component?: string): Promise<any> {
|
||||
return this.getModulePrefetchInfo(module, courseId, invalidateCache, component).then((moduleInfo) => {
|
||||
instance.size = moduleInfo.size > 0 ? moduleInfo.sizeReadable : 0;
|
||||
instance.prefetchStatusIcon = moduleInfo.statusIcon;
|
||||
|
||||
if (moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE) {
|
||||
// Module is downloadable, get the text to display to prefetch.
|
||||
if (moduleInfo.downloadTime > 0) {
|
||||
instance.prefetchText = this.translate.instant('core.lastdownloaded') + ': ' + moduleInfo.downloadTimeReadable;
|
||||
} else {
|
||||
// Module not downloaded, show a default text.
|
||||
instance.prefetchText = this.translate.instant('core.download');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof instance.statusObserver == 'undefined' && component) {
|
||||
instance.statusObserver = this.eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => {
|
||||
if (data.componentId == module.id && data.component == component) {
|
||||
this.fillContextMenu(instance, module, courseId, false, component);
|
||||
}
|
||||
}, this.sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a course download promise (if any).
|
||||
*
|
||||
|
|
|
@ -37,6 +37,7 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
|
|||
|
||||
/**
|
||||
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||
* The component returned must implement CoreCourseModuleMainComponent.
|
||||
*
|
||||
* @param {any} course The course object.
|
||||
* @param {any} module The module object.
|
||||
|
@ -67,6 +68,14 @@ export interface CoreCourseModuleHandlerData {
|
|||
*/
|
||||
class?: string;
|
||||
|
||||
/**
|
||||
* Whether to display a button to download/refresh the module if it's downloadable.
|
||||
* If it's set to true, the app will show a download/refresh button when needed and will handle the download of the
|
||||
* module using CoreCourseModulePrefetchDelegate.
|
||||
* @type {boolean}
|
||||
*/
|
||||
showDownloadButton?: boolean;
|
||||
|
||||
/**
|
||||
* The buttons to display in the module item.
|
||||
* @type {CoreCourseModuleHandlerButton[]}
|
||||
|
@ -91,6 +100,20 @@ export interface CoreCourseModuleHandlerData {
|
|||
action?(event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface that all the components to render the module in singleactivity must implement.
|
||||
*/
|
||||
export interface CoreCourseModuleMainComponent {
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} [refresher] Refresher.
|
||||
* @param {Function} [done] Function to call when done.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
doRefresh(refresher?: any, done?: () => void): Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to display in a module item.
|
||||
*/
|
||||
|
|
|
@ -202,9 +202,6 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
};
|
||||
|
||||
protected ROOT_CACHE_KEY = 'mmCourse:';
|
||||
|
||||
protected handlers: { [s: string]: CoreCourseModulePrefetchHandler } = {}; // All registered handlers.
|
||||
protected enabledHandlers: { [s: string]: CoreCourseModulePrefetchHandler } = {}; // Handlers enabled for the current site.
|
||||
protected statusCache = new CoreCache();
|
||||
|
||||
// Promises for check updates, to prevent performing the same request twice at the same time.
|
||||
|
@ -225,9 +222,14 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
private courseProvider: CoreCourseProvider, private filepoolProvider: CoreFilepoolProvider,
|
||||
private timeUtils: CoreTimeUtilsProvider, private fileProvider: CoreFileProvider,
|
||||
protected eventsProvider: CoreEventsProvider) {
|
||||
super('CoreCourseModulePrefetchDelegate', loggerProvider, sitesProvider);
|
||||
super('CoreCourseModulePrefetchDelegate', loggerProvider, sitesProvider, eventsProvider);
|
||||
|
||||
this.sitesProvider.createTableFromSchema(this.checkUpdatesTableSchema);
|
||||
|
||||
eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearStatusCache.bind(this));
|
||||
eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => {
|
||||
this.updateStatusCache(data.status, data.component, data.componentId);
|
||||
}, this.sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -656,6 +658,8 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
promise;
|
||||
|
||||
if (!refresh && typeof status != 'undefined') {
|
||||
this.storeCourseAndSection(packageId, courseId, sectionId);
|
||||
|
||||
return Promise.resolve(this.determineModuleStatus(module, status, canCheck));
|
||||
}
|
||||
|
||||
|
@ -667,7 +671,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
|
||||
// Get the saved package status.
|
||||
return this.filepoolProvider.getPackageStatus(siteId, component, module.id).then((currentStatus) => {
|
||||
status = handler.determineStatus ? handler.determineStatus(module, status, canCheck) : status;
|
||||
status = handler.determineStatus ? handler.determineStatus(module, currentStatus, canCheck) : currentStatus;
|
||||
if (status != CoreConstants.DOWNLOADED) {
|
||||
return status;
|
||||
}
|
||||
|
@ -699,7 +703,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
// Has updates, mark the module as outdated.
|
||||
status = CoreConstants.OUTDATED;
|
||||
|
||||
return this.filepoolProvider.storePackageStatus(siteId, component, module.id, status).catch(() => {
|
||||
return this.filepoolProvider.storePackageStatus(siteId, status, component, module.id).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return status;
|
||||
|
@ -713,13 +717,14 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
}, () => {
|
||||
// Error getting updates, show the stored status.
|
||||
updateStatus = false;
|
||||
this.storeCourseAndSection(packageId, courseId, sectionId);
|
||||
|
||||
return currentStatus;
|
||||
});
|
||||
});
|
||||
}).then((status) => {
|
||||
if (updateStatus) {
|
||||
this.updateStatusCache(status, courseId, component, module.id, sectionId);
|
||||
this.updateStatusCache(status, component, module.id, courseId, sectionId);
|
||||
}
|
||||
|
||||
return this.determineModuleStatus(module, status, canCheck);
|
||||
|
@ -773,11 +778,6 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
|
||||
promises.push(this.getModuleStatus(module, courseId, updates, refresh).then((modStatus) => {
|
||||
if (modStatus != CoreConstants.NOT_DOWNLOADABLE) {
|
||||
if (sectionId && sectionId > 0) {
|
||||
// Store the section ID.
|
||||
this.statusCache.setValue(packageId, 'sectionId', sectionId);
|
||||
}
|
||||
|
||||
status = this.filepoolProvider.determinePackagesStatus(status, modStatus);
|
||||
result[modStatus].push(module);
|
||||
result.total++;
|
||||
|
@ -859,7 +859,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
* @return {CoreCourseModulePrefetchHandler} Prefetch handler.
|
||||
*/
|
||||
getPrefetchHandlerFor(module: any): CoreCourseModulePrefetchHandler {
|
||||
return this.enabledHandlers[module.modname];
|
||||
return <CoreCourseModulePrefetchHandler> this.getHandler(module.modname, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1126,7 +1126,8 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
// Update status of the module.
|
||||
const packageId = this.filepoolProvider.getPackageId(handler.component, module.id);
|
||||
this.statusCache.setValue(packageId, 'downloadedSize', 0);
|
||||
this.filepoolProvider.storePackageStatus(siteId, handler.component, module.id, CoreConstants.NOT_DOWNLOADED);
|
||||
|
||||
return this.filepoolProvider.storePackageStatus(siteId, CoreConstants.NOT_DOWNLOADED, handler.component, module.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1147,6 +1148,22 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If courseId or sectionId is set, save them in the cache.
|
||||
*
|
||||
* @param {string} packageId The package ID.
|
||||
* @param {number} [courseId] Course ID.
|
||||
* @param {number} [sectionId] Section ID.
|
||||
*/
|
||||
storeCourseAndSection(packageId: string, courseId?: number, sectionId?: number): void {
|
||||
if (courseId) {
|
||||
this.statusCache.setValue(packageId, 'courseId', courseId);
|
||||
}
|
||||
if (sectionId && sectionId > 0) {
|
||||
this.statusCache.setValue(packageId, 'sectionId', sectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat the result of the check updates WS call.
|
||||
*
|
||||
|
@ -1184,12 +1201,12 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
* Update the status of a module in the "cache".
|
||||
*
|
||||
* @param {string} status New status.
|
||||
* @param {number} courseId Course ID of the module.
|
||||
* @param {string} component Package's component.
|
||||
* @param {string|number} [componentId] An ID to use in conjunction with the component.
|
||||
* @param {number} [courseId] Course ID of the module.
|
||||
* @param {number} [sectionId] Section ID of the module.
|
||||
*/
|
||||
updateStatusCache(status: string, courseId: number, component: string, componentId?: string | number, sectionId?: number)
|
||||
updateStatusCache(status: string, component: string, componentId?: string | number, courseId?: number, sectionId?: number)
|
||||
: void {
|
||||
const packageId = this.filepoolProvider.getPackageId(component, componentId),
|
||||
cachedStatus = this.statusCache.getValue(packageId, 'status', true);
|
||||
|
@ -1198,7 +1215,13 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
// If the status has changed, notify that the section has changed.
|
||||
notify = typeof cachedStatus != 'undefined' && cachedStatus !== status;
|
||||
|
||||
// If courseId/sectionId is set, store it.
|
||||
this.storeCourseAndSection(packageId, courseId, sectionId);
|
||||
|
||||
if (notify) {
|
||||
if (!courseId) {
|
||||
courseId = this.statusCache.getValue(packageId, 'courseId', true);
|
||||
}
|
||||
if (!sectionId) {
|
||||
sectionId = this.statusCache.getValue(packageId, 'sectionId', true);
|
||||
}
|
||||
|
@ -1208,8 +1231,6 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
|||
this.statusCache.setValue(packageId, 'status', status);
|
||||
|
||||
if (sectionId) {
|
||||
this.statusCache.setValue(packageId, 'sectionId', sectionId);
|
||||
|
||||
this.eventsProvider.trigger(CoreEventsProvider.SECTION_STATUS_CHANGED, {
|
||||
sectionId: sectionId,
|
||||
courseId: courseId
|
||||
|
|
|
@ -99,7 +99,8 @@ export class CoreUserProfileFieldDelegate extends CoreDelegate {
|
|||
*/
|
||||
getDataForField(field: any, signup: boolean, registerAuth: string, formValues: any): Promise<any> {
|
||||
const type = field.type || field.datatype,
|
||||
handler = this.getHandler(type, !signup);
|
||||
handler = <CoreUserProfileFieldHandler> this.getHandler(type, !signup);
|
||||
|
||||
if (handler) {
|
||||
const name = 'profile_field_' + field.shortname;
|
||||
if (handler.getData) {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||
import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core';
|
||||
import { NavController, Content } from 'ionic-angular';
|
||||
import { CoreSitesProvider } from '../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
||||
|
@ -40,7 +40,7 @@ export class CoreLinkDirective implements OnInit {
|
|||
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
|
||||
private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider,
|
||||
private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController,
|
||||
private content: Content) {
|
||||
@Optional() private content: Content) {
|
||||
// This directive can be added dynamically. In that case, the first param is the anchor HTMLElement.
|
||||
this.element = element.nativeElement || element;
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ export class CoreLinkDirective implements OnInit {
|
|||
protected navigate(href: string): void {
|
||||
const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link=';
|
||||
|
||||
if (href.indexOf('cdvfile://') === 0 || href.indexOf('file://') === 0) {
|
||||
if (href.indexOf('cdvfile://') === 0 || href.indexOf('file://') === 0 || href.indexOf('filesystem:') === 0) {
|
||||
// We have a local file.
|
||||
this.utils.openFile(href).catch((error) => {
|
||||
this.domUtils.showErrorModal(error);
|
||||
|
|
|
@ -477,7 +477,7 @@ export class CoreFilepoolProvider {
|
|||
componentId: componentId || ''
|
||||
};
|
||||
|
||||
return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, undefined);
|
||||
return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, { fileId: fileId });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1178,7 +1178,9 @@ export class CoreFilepoolProvider {
|
|||
return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress);
|
||||
}).then((response) => {
|
||||
if (typeof component != 'undefined') {
|
||||
this.addFileLink(siteId, fileId, component, componentId);
|
||||
this.addFileLink(siteId, fileId, component, componentId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
this.notifyFileDownloaded(siteId, fileId);
|
||||
|
||||
|
@ -2237,9 +2239,11 @@ export class CoreFilepoolProvider {
|
|||
}),
|
||||
whereAndParams = db.getInOrEqual(fileIds);
|
||||
|
||||
whereAndParams[0] = 'fileId ' + whereAndParams[0];
|
||||
|
||||
if (onlyUnknown) {
|
||||
whereAndParams[0] += ' AND (isexternalfile = ? OR (revision < ? AND timemodified = ?))';
|
||||
whereAndParams[1] = whereAndParams[1].params.concat([0, 1, 0]);
|
||||
whereAndParams[1] = whereAndParams[1].concat([0, 1, 0]);
|
||||
}
|
||||
|
||||
return db.updateRecordsWhere(this.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]);
|
||||
|
@ -2443,8 +2447,12 @@ export class CoreFilepoolProvider {
|
|||
if (entry && !this.isFileOutdated(entry, options.revision, options.timemodified)) {
|
||||
// We have the file, it is not stale, we can update links and remove from queue.
|
||||
this.logger.debug('Queued file already in store, ignoring...');
|
||||
this.addFileLinks(siteId, fileId, links);
|
||||
this.removeFromQueue(siteId, fileId).finally(() => {
|
||||
this.addFileLinks(siteId, fileId, links).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
this.removeFromQueue(siteId, fileId).catch(() => {
|
||||
// Ignore errors.
|
||||
}).finally(() => {
|
||||
this.treatQueueDeferred(siteId, fileId, true);
|
||||
});
|
||||
this.notifyFileDownloaded(siteId, fileId);
|
||||
|
@ -2457,7 +2465,9 @@ export class CoreFilepoolProvider {
|
|||
|
||||
return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry).then(() => {
|
||||
// Success, we add links and remove from queue.
|
||||
this.addFileLinks(siteId, fileId, links);
|
||||
this.addFileLinks(siteId, fileId, links).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
|
||||
this.treatQueueDeferred(siteId, fileId, true);
|
||||
this.notifyFileDownloaded(siteId, fileId);
|
||||
|
|
|
@ -32,9 +32,12 @@ export class CoreDomUtilsProvider {
|
|||
// List of input types that support keyboard.
|
||||
protected INPUT_SUPPORT_KEYBOARD = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password',
|
||||
'search', 'tel', 'text', 'time', 'url', 'week'];
|
||||
protected INSTANCE_ID_ATTR_NAME = 'core-instance-id';
|
||||
|
||||
protected element = document.createElement('div'); // Fake element to use in some functions, to prevent creating it each time.
|
||||
protected matchesFn: string; // Name of the "matches" function to use when simulating a closest call.
|
||||
protected instances: {[id: string]: any} = {}; // Store component/directive instances by id.
|
||||
protected lastInstanceId = 0;
|
||||
|
||||
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
|
||||
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
|
||||
|
@ -410,6 +413,20 @@ export class CoreDomUtilsProvider {
|
|||
return this.textUtils.decodeHTML(this.translate.instant('core.error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve component/directive instance.
|
||||
* Please use this function only if you cannot retrieve the instance using parent/child methods: ViewChild (or similar)
|
||||
* or Angular's injection.
|
||||
*
|
||||
* @param {Element} element The root element of the component/directive.
|
||||
* @return {any} The instance, undefined if not found.
|
||||
*/
|
||||
getInstanceByElement(element: Element): any {
|
||||
const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME);
|
||||
|
||||
return this.instances[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element is outside of screen (viewport).
|
||||
*
|
||||
|
@ -513,6 +530,25 @@ export class CoreDomUtilsProvider {
|
|||
return this.element.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a component/directive instance using the DOM Element.
|
||||
*
|
||||
* @param {Element} element The root element of the component/directive.
|
||||
*/
|
||||
removeInstanceByElement(element: Element): void {
|
||||
const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME);
|
||||
delete this.instances[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a component/directive instance using the ID.
|
||||
*
|
||||
* @param {string} id The ID to remove.
|
||||
*/
|
||||
removeInstanceById(id: string): void {
|
||||
delete this.instances[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for certain classes in an element contents and replace them with the specified new values.
|
||||
*
|
||||
|
@ -547,28 +583,26 @@ export class CoreDomUtilsProvider {
|
|||
|
||||
// Treat elements with src (img, audio, video, ...).
|
||||
media = this.element.querySelectorAll('img, video, audio, source, track');
|
||||
for (const i in media) {
|
||||
const el = media[i];
|
||||
let newSrc = paths[this.textUtils.decodeURIComponent(el.getAttribute('src'))];
|
||||
media.forEach((media: HTMLElement) => {
|
||||
let newSrc = paths[this.textUtils.decodeURIComponent(media.getAttribute('src'))];
|
||||
|
||||
if (typeof newSrc != 'undefined') {
|
||||
el.setAttribute('src', newSrc);
|
||||
media.setAttribute('src', newSrc);
|
||||
}
|
||||
|
||||
// Treat video posters.
|
||||
if (el.tagName == 'VIDEO' && el.getAttribute('poster')) {
|
||||
newSrc = paths[this.textUtils.decodeURIComponent(el.getAttribute('poster'))];
|
||||
if (media.tagName == 'VIDEO' && media.getAttribute('poster')) {
|
||||
newSrc = paths[this.textUtils.decodeURIComponent(media.getAttribute('poster'))];
|
||||
if (typeof newSrc !== 'undefined') {
|
||||
el.setAttribute('poster', newSrc);
|
||||
media.setAttribute('poster', newSrc);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Now treat links.
|
||||
anchors = this.element.querySelectorAll('a');
|
||||
for (const i in anchors) {
|
||||
const anchor = anchors[i],
|
||||
href = this.textUtils.decodeURIComponent(anchor.getAttribute('href')),
|
||||
anchors.forEach((anchor: HTMLElement) => {
|
||||
const href = this.textUtils.decodeURIComponent(anchor.getAttribute('href')),
|
||||
newUrl = paths[href];
|
||||
|
||||
if (typeof newUrl != 'undefined') {
|
||||
|
@ -578,7 +612,7 @@ export class CoreDomUtilsProvider {
|
|||
anchorFn(anchor, href);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this.element.innerHTML;
|
||||
}
|
||||
|
@ -885,6 +919,22 @@ export class CoreDomUtilsProvider {
|
|||
return loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a component/directive instance.
|
||||
*
|
||||
* @param {Element} element The root element of the component/directive.
|
||||
* @param {any} instance The instance to store.
|
||||
* @return {string} ID to identify the instance.
|
||||
*/
|
||||
storeInstanceByElement(element: Element, instance: any): string {
|
||||
const id = String(this.lastInstanceId++);
|
||||
|
||||
element.setAttribute(this.INSTANCE_ID_ATTR_NAME, id);
|
||||
this.instances[id] = instance;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element supports input via keyboard.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue