forked from EVOgeek/Vmeda.Online
		
	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)"> |             previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)"> | ||||||
|         </core-navigation-bar> |         </core-navigation-bar> | ||||||
| 
 | 
 | ||||||
|         <div class="ion-padding"> |         <ion-slides (ionSlideWillChange)="slideChanged()" [options]="slidesOpts"> | ||||||
|             <core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module" |             <ion-slide *ngFor="let chapter of loadedChapters"> | ||||||
|                 [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> |                 <div class="ion-padding"> | ||||||
| 
 |                     <core-format-text [component]="component" [componentId]="componentId" [text]="chapter.content" contextLevel="module" | ||||||
|             <div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0"> |                         [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> | ||||||
|                 <strong>{{ 'core.tag.tags' | translate }}: </strong> |                     <div class="ion-margin-top" *ngIf="tagsEnabled && chapter.tags?.length > 0"> | ||||||
|                 <core-tag-list [tags]="tags"></core-tag-list> |                         <strong>{{ 'core.tag.tags' | translate }}: </strong> | ||||||
|             </div> |                         <core-tag-list [tags]="chapter.tags"></core-tag-list> | ||||||
|         </div> |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </ion-slide> | ||||||
|  |         </ion-slides> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
| </core-loading> | </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
 | // 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; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Show chapter.
 |             await this.loadChapters(); | ||||||
|             try { |  | ||||||
|                 await this.loadChapter(this.currentChapter, refresh); |  | ||||||
| 
 | 
 | ||||||
|                 this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; |             // Show chapter.
 | ||||||
|             } catch { |             await this.viewChapter(this.currentChapter, refresh); | ||||||
|                 // Ignore errors, they're handled inside the loadChapter function.
 | 
 | ||||||
|             } |             this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; | ||||||
|         } 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,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 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 { |         if (this.displayNavBar) { | ||||||
|             const content = await AddonModBook.getChapterContent(this.contentsMap, chapterId, this.module.id); |             this.navigationItems = this.getNavigationItems(chapterId); | ||||||
| 
 |  | ||||||
|             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; |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         // 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 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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user