MOBILE-3927 book: Add swipe to book

main
Dani Palou 2021-11-23 11:23:31 +01:00
parent 4ec6096482
commit 9bf939ab2f
4 changed files with 151 additions and 63 deletions

View File

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

View File

@ -0,0 +1,9 @@
:host {
ion-slide {
display: block;
font-size: inherit;
justify-content: start;
align-items: start;
text-align: start;
}
}

View File

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

View File

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