MOBILE-3927 book: Add swipe to book
This commit is contained in:
		
							parent
							
								
									4ec6096482
								
							
						
					
					
						commit
						9bf939ab2f
					
				| @ -40,15 +40,18 @@ | ||||
|             previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)"> | ||||
|         </core-navigation-bar> | ||||
| 
 | ||||
|         <div class="ion-padding"> | ||||
|             <core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module" | ||||
|                 [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> | ||||
| 
 | ||||
|             <div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0"> | ||||
|                 <strong>{{ 'core.tag.tags' | translate }}: </strong> | ||||
|                 <core-tag-list [tags]="tags"></core-tag-list> | ||||
|             </div> | ||||
|         </div> | ||||
|         <ion-slides (ionSlideWillChange)="slideChanged()" [options]="slidesOpts"> | ||||
|             <ion-slide *ngFor="let chapter of loadedChapters"> | ||||
|                 <div class="ion-padding"> | ||||
|                     <core-format-text [component]="component" [componentId]="componentId" [text]="chapter.content" contextLevel="module" | ||||
|                         [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> | ||||
|                     <div class="ion-margin-top" *ngIf="tagsEnabled && chapter.tags?.length > 0"> | ||||
|                         <strong>{{ 'core.tag.tags' | translate }}: </strong> | ||||
|                         <core-tag-list [tags]="chapter.tags"></core-tag-list> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ion-slide> | ||||
|         </ion-slides> | ||||
|     </div> | ||||
| 
 | ||||
| </core-loading> | ||||
|  | ||||
							
								
								
									
										9
									
								
								src/addons/mod/book/components/index/index.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/addons/mod/book/components/index/index.scss
									
									
									
									
									
										Normal file
									
								
							| @ -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
 | ||||
| // 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<AddonModBookTocChapter>[] = []; | ||||
|     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<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 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<void> { | ||||
|     protected async viewChapter(chapterId: number, logChapterId: boolean): Promise<void> { | ||||
|         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<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 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; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user