diff --git a/src/addons/mod/book/components/index/addon-mod-book-index.html b/src/addons/mod/book/components/index/addon-mod-book-index.html index ce5420250..e85705f95 100644 --- a/src/addons/mod/book/components/index/addon-mod-book-index.html +++ b/src/addons/mod/book/components/index/addon-mod-book-index.html @@ -40,15 +40,18 @@ previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)"> -
- - -
- {{ 'core.tag.tags' | translate }}: - -
-
+ + +
+ +
+ {{ 'core.tag.tags' | translate }}: + +
+
+
+
diff --git a/src/addons/mod/book/components/index/index.scss b/src/addons/mod/book/components/index/index.scss new file mode 100644 index 000000000..6b0b7043d --- /dev/null +++ b/src/addons/mod/book/components/index/index.scss @@ -0,0 +1,9 @@ +:host { + ion-slide { + display: block; + font-size: inherit; + justify-content: start; + align-items: start; + text-align: start; + } +} diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index 5d3281c25..34ac81fa0 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Optional, Input, OnInit } from '@angular/core'; -import { IonContent } from '@ionic/angular'; +import { Component, Optional, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { IonContent, IonSlides } from '@ionic/angular'; import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; import { AddonModBookProvider, @@ -31,6 +31,8 @@ import { CoreCourse } from '@features/course/services/course'; import { AddonModBookTocComponent } from '../toc/toc'; import { CoreConstants } from '@/core/constants'; import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; +import { CoreError } from '@classes/errors/error'; +import { Translate } from '@singletons'; /** * Component that displays a book. @@ -38,30 +40,43 @@ import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar @Component({ selector: 'addon-mod-book-index', templateUrl: 'addon-mod-book-index.html', + styleUrls: ['index.scss'], }) export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { + @ViewChild(IonSlides) slides?: IonSlides; + @Input() initialChapterId?: number; // The initial chapter ID to load. component = AddonModBookProvider.COMPONENT; - chapterContent?: string; + loadedChapters: LoadedChapter[] = []; + previousChapter?: AddonModBookTocChapter; + nextChapter?: AddonModBookTocChapter; tagsEnabled = false; warning = ''; tags?: CoreTagItem[]; displayNavBar = true; navigationItems: CoreNavigationBarItem[] = []; displayTitlesInNavBar = false; + slidesOpts = { + initialSlide: 0, + autoHeight: true, + }; protected chapters: AddonModBookTocChapter[] = []; protected currentChapter?: number; protected book?: AddonModBookBookWSData; protected contentsMap: AddonModBookContentsMap = {}; + protected element: HTMLElement; constructor( + elementRef: ElementRef, protected content?: IonContent, @Optional() courseContentsPage?: CoreCourseContentsPage, ) { super('AddonModBookIndexComponent', courseContentsPage); + + this.element = elementRef.nativeElement; } /** @@ -102,10 +117,13 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp * @return Promise resolved when done. */ changeChapter(chapterId: number): void { - if (chapterId && chapterId != this.currentChapter) { - this.loaded = false; - this.refreshIcon = CoreConstants.ICON_LOADING; - this.loadChapter(chapterId, true); + if (!chapterId || chapterId === this.currentChapter) { + return; + } + + 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) { // 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.slidesOpts.initialSlide = index; } } @@ -154,14 +173,12 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp return; } - // Show chapter. - try { - await this.loadChapter(this.currentChapter, refresh); + await this.loadChapters(); - this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; - } catch { - // Ignore errors, they're handled inside the loadChapter function. - } + // Show chapter. + await this.viewChapter(this.currentChapter, refresh); + + this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; } finally { // Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true. this.fillContextMenu(false); @@ -184,49 +201,94 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp } /** - * Load a book chapter. + * Load book chapters. + * + * @return Promise resolved when done. + */ + protected async loadChapters(): Promise { + 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 logChapterId Whether chapter ID should be passed to the log view function. * @return Promise resolved when done. */ - protected async loadChapter(chapterId: number, logChapterId: boolean): Promise { + protected async viewChapter(chapterId: number, logChapterId: boolean): Promise { 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) { - 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. - await CoreUtils.ignoreErrors(AddonModBook.logView( - this.module.instance!, - logChapterId ? chapterId : undefined, - this.module.name, - )); - - const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); - const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined; - - // Module is completed when last chapter is viewed, so we only check completion if the last is reached. - if (isLastChapter) { - 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; + if (this.displayNavBar) { + this.navigationItems = this.getNavigationItems(chapterId); } + + // Chapter loaded, log view. + await CoreUtils.ignoreErrors(AddonModBook.logView( + this.module.instance!, + logChapterId ? chapterId : undefined, + this.module.name, + )); + + const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); + const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined; + + // Module is completed when last chapter is viewed, so we only check completion if the last is reached. + if (isLastChapter) { + CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); + } + } + + /** + * Slide has changed. + * + * @return Promise resolved when done. + */ + async slideChanged(): Promise { + if (!this.slides) { + return; + } + + const scrollElement = await this.content?.getScrollElement(); + const container = this.element.querySelector('.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[]; +}; diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index b3052137e..ba8d78d07 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -769,21 +769,29 @@ export class CoreDomUtilsProvider { * * @param scrollEl The element that must be scrolled. * @param element DOM element to check. + * @param point The point of the element to check. * @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(); if (!elementRect) { 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 scrollTopPos = scrollElRect?.top || 0; - return elementMidPoint > window.innerHeight || elementMidPoint < scrollTopPos; + return elementPoint > window.innerHeight || elementPoint < scrollTopPos; } /**