Merge pull request #1243 from crazyserver/dpalou-MOBILE-2335

Dpalou mobile 2335
main
Juan Leyva 2018-02-09 15:31:08 +01:00 committed by GitHub
commit 67f8b32c42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1897 additions and 131 deletions

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
{
"errorchapter": "Error reading chapter of book."
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
a.core-course-module-handler.addon-mod-label-handler {
align-items: center;
}

View File

@ -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.

View File

@ -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],

View File

@ -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];
}

View File

@ -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 = [],

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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.
*

View File

@ -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);
}
/**

View File

@ -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.

View File

@ -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);

View File

@ -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();

View File

@ -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>

View File

@ -0,0 +1,6 @@
ion-badge.core-course-download-section-progress {
display: block;
float: left;
margin-top: 12px;
margin-right: 12px;
}

View File

@ -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.
*/

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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]));
}
}

View File

@ -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.
*

View File

@ -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>

View File

@ -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();
});
});
});
}

View File

@ -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.
*

View File

@ -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.
*

View File

@ -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).
*

View File

@ -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.
*/

View File

@ -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

View File

@ -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) {

View File

@ -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);

View File

@ -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);

View File

@ -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.
*