MOBILE-3927 book: Add swipe to book
parent
4ec6096482
commit
9bf939ab2f
|
@ -40,15 +40,18 @@
|
||||||
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)">
|
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)">
|
||||||
</core-navigation-bar>
|
</core-navigation-bar>
|
||||||
|
|
||||||
|
<ion-slides (ionSlideWillChange)="slideChanged()" [options]="slidesOpts">
|
||||||
|
<ion-slide *ngFor="let chapter of loadedChapters">
|
||||||
<div class="ion-padding">
|
<div class="ion-padding">
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module"
|
<core-format-text [component]="component" [componentId]="componentId" [text]="chapter.content" contextLevel="module"
|
||||||
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
|
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
|
||||||
|
<div class="ion-margin-top" *ngIf="tagsEnabled && chapter.tags?.length > 0">
|
||||||
<div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0">
|
|
||||||
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
||||||
<core-tag-list [tags]="tags"></core-tag-list>
|
<core-tag-list [tags]="chapter.tags"></core-tag-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ion-slide>
|
||||||
|
</ion-slides>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
:host {
|
||||||
|
ion-slide {
|
||||||
|
display: block;
|
||||||
|
font-size: inherit;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: start;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,8 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, Optional, Input, OnInit } from '@angular/core';
|
import { Component, Optional, Input, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||||
import { IonContent } from '@ionic/angular';
|
import { IonContent, IonSlides } from '@ionic/angular';
|
||||||
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
|
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
|
||||||
import {
|
import {
|
||||||
AddonModBookProvider,
|
AddonModBookProvider,
|
||||||
|
@ -31,6 +31,8 @@ import { CoreCourse } from '@features/course/services/course';
|
||||||
import { AddonModBookTocComponent } from '../toc/toc';
|
import { AddonModBookTocComponent } from '../toc/toc';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a book.
|
* Component that displays a book.
|
||||||
|
@ -38,30 +40,43 @@ import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'addon-mod-book-index',
|
selector: 'addon-mod-book-index',
|
||||||
templateUrl: 'addon-mod-book-index.html',
|
templateUrl: 'addon-mod-book-index.html',
|
||||||
|
styleUrls: ['index.scss'],
|
||||||
})
|
})
|
||||||
export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
|
export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
|
||||||
|
|
||||||
|
@ViewChild(IonSlides) slides?: IonSlides;
|
||||||
|
|
||||||
@Input() initialChapterId?: number; // The initial chapter ID to load.
|
@Input() initialChapterId?: number; // The initial chapter ID to load.
|
||||||
|
|
||||||
component = AddonModBookProvider.COMPONENT;
|
component = AddonModBookProvider.COMPONENT;
|
||||||
chapterContent?: string;
|
loadedChapters: LoadedChapter[] = [];
|
||||||
|
previousChapter?: AddonModBookTocChapter;
|
||||||
|
nextChapter?: AddonModBookTocChapter;
|
||||||
tagsEnabled = false;
|
tagsEnabled = false;
|
||||||
warning = '';
|
warning = '';
|
||||||
tags?: CoreTagItem[];
|
tags?: CoreTagItem[];
|
||||||
displayNavBar = true;
|
displayNavBar = true;
|
||||||
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
|
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
|
||||||
displayTitlesInNavBar = false;
|
displayTitlesInNavBar = false;
|
||||||
|
slidesOpts = {
|
||||||
|
initialSlide: 0,
|
||||||
|
autoHeight: true,
|
||||||
|
};
|
||||||
|
|
||||||
protected chapters: AddonModBookTocChapter[] = [];
|
protected chapters: AddonModBookTocChapter[] = [];
|
||||||
protected currentChapter?: number;
|
protected currentChapter?: number;
|
||||||
protected book?: AddonModBookBookWSData;
|
protected book?: AddonModBookBookWSData;
|
||||||
protected contentsMap: AddonModBookContentsMap = {};
|
protected contentsMap: AddonModBookContentsMap = {};
|
||||||
|
protected element: HTMLElement;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
elementRef: ElementRef,
|
||||||
protected content?: IonContent,
|
protected content?: IonContent,
|
||||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||||
) {
|
) {
|
||||||
super('AddonModBookIndexComponent', courseContentsPage);
|
super('AddonModBookIndexComponent', courseContentsPage);
|
||||||
|
|
||||||
|
this.element = elementRef.nativeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,10 +117,13 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
changeChapter(chapterId: number): void {
|
changeChapter(chapterId: number): void {
|
||||||
if (chapterId && chapterId != this.currentChapter) {
|
if (!chapterId || chapterId === this.currentChapter) {
|
||||||
this.loaded = false;
|
return;
|
||||||
this.refreshIcon = CoreConstants.ICON_LOADING;
|
}
|
||||||
this.loadChapter(chapterId, true);
|
|
||||||
|
const index = this.loadedChapters.findIndex(chapter => chapter.id === chapterId);
|
||||||
|
if (index > -1) {
|
||||||
|
this.slides?.slideTo(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,10 +156,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
|
|
||||||
if (typeof this.currentChapter == 'undefined' && typeof this.initialChapterId != 'undefined' && this.chapters) {
|
if (typeof this.currentChapter == 'undefined' && typeof this.initialChapterId != 'undefined' && this.chapters) {
|
||||||
// Initial chapter set. Validate that the chapter exists.
|
// Initial chapter set. Validate that the chapter exists.
|
||||||
const chapter = this.chapters.find((chapter) => chapter.id == this.initialChapterId);
|
const index = this.chapters.findIndex((chapter) => chapter.id == this.initialChapterId);
|
||||||
|
|
||||||
if (chapter) {
|
if (index >= 0) {
|
||||||
this.currentChapter = this.initialChapterId;
|
this.currentChapter = this.initialChapterId;
|
||||||
|
this.slidesOpts.initialSlide = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,14 +173,12 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.loadChapters();
|
||||||
|
|
||||||
// Show chapter.
|
// Show chapter.
|
||||||
try {
|
await this.viewChapter(this.currentChapter, refresh);
|
||||||
await this.loadChapter(this.currentChapter, refresh);
|
|
||||||
|
|
||||||
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
|
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
|
||||||
} catch {
|
|
||||||
// Ignore errors, they're handled inside the loadChapter function.
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
// Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
|
// Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
|
||||||
this.fillContextMenu(false);
|
this.fillContextMenu(false);
|
||||||
|
@ -184,28 +201,58 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a book chapter.
|
* Load book chapters.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadChapters(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const newChapters = await Promise.all(this.chapters.map(async (chapter) => {
|
||||||
|
const content = await AddonModBook.getChapterContent(this.contentsMap, chapter.id, this.module.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: chapter.id,
|
||||||
|
content,
|
||||||
|
tags: this.tagsEnabled ? this.contentsMap[chapter.id].tags : [],
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
let newIndex = -1;
|
||||||
|
if (this.loadedChapters.length && newChapters.length != this.loadedChapters.length) {
|
||||||
|
// Number of chapters has changed. Search the chapter to display, otherwise it could change automatically.
|
||||||
|
newIndex = this.chapters.findIndex((chapter) => chapter.id === this.currentChapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadedChapters = newChapters;
|
||||||
|
|
||||||
|
if (newIndex > -1) {
|
||||||
|
this.slides?.slideTo(newIndex, 0, false);
|
||||||
|
}
|
||||||
|
} catch (exception) {
|
||||||
|
const error = exception ?? new CoreError(Translate.instant('addon.mod_book.errorchapter'));
|
||||||
|
if (!error.message) {
|
||||||
|
error.message = Translate.instant('addon.mod_book.errorchapter');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View a book chapter.
|
||||||
*
|
*
|
||||||
* @param chapterId Chapter to load.
|
* @param chapterId Chapter to load.
|
||||||
* @param logChapterId Whether chapter ID should be passed to the log view function.
|
* @param logChapterId Whether chapter ID should be passed to the log view function.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadChapter(chapterId: number, logChapterId: boolean): Promise<void> {
|
protected async viewChapter(chapterId: number, logChapterId: boolean): Promise<void> {
|
||||||
this.currentChapter = chapterId;
|
this.currentChapter = chapterId;
|
||||||
this.content?.scrollToTop();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = await AddonModBook.getChapterContent(this.contentsMap, chapterId, this.module.id);
|
|
||||||
|
|
||||||
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
|
|
||||||
|
|
||||||
this.chapterContent = content;
|
|
||||||
|
|
||||||
if (this.displayNavBar) {
|
if (this.displayNavBar) {
|
||||||
this.navigationItems = this.getNavigationItems(chapterId);
|
this.navigationItems = this.getNavigationItems(chapterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
|
// Chapter loaded, log view.
|
||||||
await CoreUtils.ignoreErrors(AddonModBook.logView(
|
await CoreUtils.ignoreErrors(AddonModBook.logView(
|
||||||
this.module.instance!,
|
this.module.instance!,
|
||||||
logChapterId ? chapterId : undefined,
|
logChapterId ? chapterId : undefined,
|
||||||
|
@ -219,14 +266,29 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
if (isLastChapter) {
|
if (isLastChapter) {
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_book.errorchapter', true);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.loaded = true;
|
|
||||||
this.refreshIcon = CoreConstants.ICON_REFRESH;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slide has changed.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async slideChanged(): Promise<void> {
|
||||||
|
if (!this.slides) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollElement = await this.content?.getScrollElement();
|
||||||
|
const container = this.element.querySelector<HTMLElement>('.addon-mod_book-container');
|
||||||
|
|
||||||
|
if (container && (!scrollElement || CoreDomUtils.isElementOutsideOfScreen(scrollElement, container, 'top'))) {
|
||||||
|
// Scroll to top.
|
||||||
|
container.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = await this.slides.getActiveIndex();
|
||||||
|
|
||||||
|
this.viewChapter(this.loadedChapters[index].id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -245,3 +307,9 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoadedChapter = {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
tags?: CoreTagItem[];
|
||||||
|
};
|
||||||
|
|
|
@ -769,21 +769,29 @@ export class CoreDomUtilsProvider {
|
||||||
*
|
*
|
||||||
* @param scrollEl The element that must be scrolled.
|
* @param scrollEl The element that must be scrolled.
|
||||||
* @param element DOM element to check.
|
* @param element DOM element to check.
|
||||||
|
* @param point The point of the element to check.
|
||||||
* @return Whether the element is outside of the viewport.
|
* @return Whether the element is outside of the viewport.
|
||||||
*/
|
*/
|
||||||
isElementOutsideOfScreen(scrollEl: HTMLElement, element: HTMLElement): boolean {
|
isElementOutsideOfScreen(scrollEl: HTMLElement, element: HTMLElement, point: 'top' | 'mid' | 'bottom' = 'mid'): boolean {
|
||||||
const elementRect = element.getBoundingClientRect();
|
const elementRect = element.getBoundingClientRect();
|
||||||
|
|
||||||
if (!elementRect) {
|
if (!elementRect) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementMidPoint = Math.round((elementRect.bottom + elementRect.top) / 2);
|
let elementPoint: number;
|
||||||
|
if (point === 'top') {
|
||||||
|
elementPoint = elementRect.top;
|
||||||
|
} else if (point === 'bottom') {
|
||||||
|
elementPoint = elementRect.bottom;
|
||||||
|
} else {
|
||||||
|
elementPoint = Math.round((elementRect.bottom + elementRect.top) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
const scrollElRect = scrollEl.getBoundingClientRect();
|
const scrollElRect = scrollEl.getBoundingClientRect();
|
||||||
const scrollTopPos = scrollElRect?.top || 0;
|
const scrollTopPos = scrollElRect?.top || 0;
|
||||||
|
|
||||||
return elementMidPoint > window.innerHeight || elementMidPoint < scrollTopPos;
|
return elementPoint > window.innerHeight || elementPoint < scrollTopPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue