MOBILE-3025 blocks: Merge blocks and content scroll
This commit is contained in:
		
							parent
							
								
									9ea37af4c9
								
							
						
					
					
						commit
						8a3acc2dfd
					
				| @ -226,6 +226,7 @@ ion-app.app-root { | ||||
|     user-select: text; | ||||
|     word-break: break-word; | ||||
|     word-wrap: break-word; | ||||
|     white-space: normal; | ||||
| 
 | ||||
|     &[maxHeight], | ||||
|     &[ng-reflect-max-height] { | ||||
|  | ||||
| @ -103,7 +103,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Init editor | ||||
|      * Init editor. | ||||
|      */ | ||||
|     ngAfterContentInit(): void { | ||||
|         this.domUtils.isRichTextEditorEnabled().then((enabled) => { | ||||
|  | ||||
| @ -121,18 +121,14 @@ export class CoreTabComponent implements OnInit, OnDestroy { | ||||
|         this.showHideNavBarButtons(true); | ||||
| 
 | ||||
|         // Setup tab scrolling.
 | ||||
|         setTimeout(() => { | ||||
|             // Workaround to solve undefined this.scroll on tab change.
 | ||||
|             const scroll: HTMLElement = this.content ? this.content.getScrollElement() : | ||||
|                 this.element.querySelector('ion-content > .scroll-content'); | ||||
| 
 | ||||
|             if (scroll) { | ||||
|                 scroll.onscroll = (e): void => { | ||||
|         this.domUtils.waitElementToExist(() => this.content ? this.content.getScrollElement() : | ||||
|                 this.element.querySelector('ion-content > .scroll-content')).then((scroll) => { | ||||
|             scroll.addEventListener('scroll', (e): void => { | ||||
|                 this.tabs.showHideTabs(e.target); | ||||
|                 }; | ||||
|             }); | ||||
| 
 | ||||
|             this.tabs.showHideTabs(scroll); | ||||
|             } | ||||
|         }, 1); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| ion-app.app-root core-block { | ||||
|   position: relative; | ||||
|   display: block; | ||||
| 
 | ||||
|   core-loading.core-loading-center { | ||||
|     display: block; | ||||
|  | ||||
| @ -2,7 +2,8 @@ | ||||
|     <ng-content></ng-content> | ||||
| </div> | ||||
| 
 | ||||
| <ion-content *ngIf="blocks && blocks.length > 0" [class.core-hide-blocks]="hideBlocks" class="core-course-blocks-side"> | ||||
| <div *ngIf="blocks && blocks.length > 0" [class.core-hide-blocks]="hideBlocks" class="core-course-blocks-side"> | ||||
|     <div class="core-course-blocks-side-scroll"> | ||||
|         <core-loading [hideUntil]="dataLoaded" class="core-loading-center"> | ||||
|             <ion-list> | ||||
|                 <!-- Course blocks. --> | ||||
| @ -11,4 +12,5 @@ | ||||
|                 </ng-container> | ||||
|             </ion-list> | ||||
|         </core-loading> | ||||
| </ion-content> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| @ -1,8 +1,5 @@ | ||||
| $core-side-blocks-max-small-width: 300px; | ||||
| $core-side-blocks-min-small-width: 25%; | ||||
| 
 | ||||
| $core-side-blocks-max-width: 320px; | ||||
| $core-side-blocks-min-width: 30%; | ||||
| $core-side-blocks-max-width: 30%; | ||||
| $core-side-blocks-min-width: 280px; | ||||
| 
 | ||||
| .core-course-block-with-blocks > .scroll-content { | ||||
|     overflow-y: visible; | ||||
| @ -10,15 +7,8 @@ $core-side-blocks-min-width: 30%; | ||||
| 
 | ||||
| ion-app.app-root core-block-course-blocks { | ||||
| 
 | ||||
|     &.core-no-blocks { | ||||
|         .core-course-blocks-content > ion-content { | ||||
|     &.core-no-blocks .core-course-blocks-content { | ||||
|         height: auto; | ||||
| 
 | ||||
|             > .scroll-content { | ||||
|                 overflow-y: visible; | ||||
|                 position: relative; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     &.core-has-blocks { | ||||
| @ -33,49 +23,51 @@ ion-app.app-root core-block-course-blocks { | ||||
|             flex-wrap: nowrap; | ||||
| 
 | ||||
|             .core-course-blocks-content { | ||||
|                 min-width: calc(100% - #{($core-side-blocks-max-small-width)}); | ||||
|                 max-width: calc(100% - #{($core-side-blocks-min-small-width)}); | ||||
|                 z-index: 0; | ||||
|                 flex: 1; | ||||
|                 box-shadow: none !important; | ||||
|                 flex-grow: 1; | ||||
|                 max-width: 100%; | ||||
|             } | ||||
| 
 | ||||
|             ion-content.core-course-blocks-side { | ||||
|                 transform: none !important; | ||||
|                 position: sticky; | ||||
|                 @include position(0, 0, 0, auto); | ||||
|                 z-index: 30; | ||||
|                 max-width: $core-side-blocks-max-small-width; | ||||
|                 min-width: $core-side-blocks-min-small-width; | ||||
|                 @include border-start(1px, solid, $list-md-border-color); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @include media-breakpoint-up(lg) { | ||||
|             .core-course-blocks-content { | ||||
|                 min-width: calc(100% - #{($core-side-blocks-max-width)}); | ||||
|                 max-width: calc(100% - #{($core-side-blocks-min-width)}); | ||||
|             } | ||||
| 
 | ||||
|             ion-content.core-course-blocks-side { | ||||
|             div.core-course-blocks-side { | ||||
|                 position: relative; | ||||
|                 @include position(auto, 0, auto, auto); | ||||
|                 max-width: $core-side-blocks-max-width; | ||||
|                 min-width: $core-side-blocks-min-width; | ||||
|                 @include border-start(1px, solid, $list-md-border-color); | ||||
| 
 | ||||
|                 .core-course-blocks-side-scroll { | ||||
|                     position: absolute; | ||||
|                     top: 0; | ||||
|                     max-width: 100%; | ||||
|                     min-width: 100%; | ||||
| 
 | ||||
|                     &.core-course-blocks-fixed-bottom { | ||||
|                         position: fixed; | ||||
|                         bottom: 0; | ||||
|                         top: auto; | ||||
|                         transform: none !important; | ||||
|                     } | ||||
| 
 | ||||
|                     core-block { | ||||
|                         max-width: $core-side-blocks-max-width; | ||||
|                         min-width: $core-side-blocks-min-width; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @include media-breakpoint-down(sm) { | ||||
|             // Disable scroll on individual columns. | ||||
|             .core-course-blocks-content > ion-content, | ||||
|             ion-content.core-course-blocks-side { | ||||
|             div.core-course-blocks-side { | ||||
|                 transform: none !important; | ||||
|                 height: auto; | ||||
| 
 | ||||
|                 &.core-hide-blocks { | ||||
|                     display: none; | ||||
|                 } | ||||
| 
 | ||||
|                 > .scroll-content { | ||||
|                     overflow-y: visible; | ||||
|                     position: relative; | ||||
|                 .core-course-blocks-side-scroll { | ||||
|                     transform: none !important; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -12,9 +12,10 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef, Optional } from '@angular/core'; | ||||
| import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef, OnDestroy } from '@angular/core'; | ||||
| import { Content } from 'ionic-angular'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreBlockComponent } from '../block/block'; | ||||
| import { CoreBlockHelperProvider } from '../../providers/helper'; | ||||
| @ -26,7 +27,7 @@ import { CoreBlockHelperProvider } from '../../providers/helper'; | ||||
|     selector: 'core-block-course-blocks', | ||||
|     templateUrl: 'core-block-course-blocks.html', | ||||
| }) | ||||
| export class CoreBlockCourseBlocksComponent implements OnInit { | ||||
| export class CoreBlockCourseBlocksComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @Input() courseId: number; | ||||
|     @Input() hideBlocks = false; | ||||
| @ -38,13 +39,17 @@ export class CoreBlockCourseBlocksComponent implements OnInit { | ||||
|     blocks = []; | ||||
| 
 | ||||
|     protected element: HTMLElement; | ||||
|     protected parentContent: HTMLElement; | ||||
|     protected lastScroll; | ||||
|     protected translationY = 0; | ||||
|     protected blocksScrollHeight = 0; | ||||
|     protected sideScroll: HTMLElement; | ||||
|     protected vpHeight = 0; // Viewport height.
 | ||||
|     protected scrollWorking = false; | ||||
| 
 | ||||
|     constructor(private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, | ||||
|             protected blockHelper: CoreBlockHelperProvider, element: ElementRef, | ||||
|             @Optional() content: Content) { | ||||
|             protected content: Content, protected appProvider: CoreAppProvider) { | ||||
|         this.element = element.nativeElement; | ||||
|         this.parentContent = content.getElementRef().nativeElement; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -53,9 +58,77 @@ export class CoreBlockCourseBlocksComponent implements OnInit { | ||||
|     ngOnInit(): void { | ||||
|         this.loadContent().finally(() => { | ||||
|             this.dataLoaded = true; | ||||
| 
 | ||||
|             window.addEventListener('resize', this.initScroll.bind(this)); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Setup scrolling. | ||||
|      */ | ||||
|     protected initScroll(): void { | ||||
|         const scroll: HTMLElement = this.content && this.content.getScrollElement(); | ||||
| 
 | ||||
|         this.domUtils.waitElementToExist(() => scroll && scroll.querySelector('.core-course-blocks-side')).then((sideElement) => { | ||||
|             const contentHeight = parseInt(this.content.getNativeElement().querySelector('.scroll-content').scrollHeight, 10); | ||||
| 
 | ||||
|             this.sideScroll = scroll.querySelector('.core-course-blocks-side-scroll'); | ||||
|             this.blocksScrollHeight = this.sideScroll.scrollHeight; | ||||
|             this.vpHeight = sideElement.clientHeight; | ||||
| 
 | ||||
|             // Check if needed and event was not init before.
 | ||||
|             if (this.appProvider.isWide() && this.vpHeight && contentHeight > this.vpHeight && | ||||
|                     this.blocksScrollHeight > this.vpHeight) { | ||||
|                 if (typeof this.lastScroll == 'undefined') { | ||||
|                     this.lastScroll = 0; | ||||
|                     scroll.addEventListener('scroll', this.scrollFunction.bind(this)); | ||||
|                 } | ||||
|                 this.scrollWorking = true; | ||||
|             } else { | ||||
|                 this.sideScroll.style.transform = 'translate(0, 0)'; | ||||
|                 this.sideScroll.classList.remove('core-course-blocks-fixed-bottom'); | ||||
|                 this.scrollWorking = false; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Scroll function that moves the sidebar if needed. | ||||
|      * | ||||
|      * @param {Event} e Event to get the target from. | ||||
|      */ | ||||
|     protected scrollFunction(e: Event): void { | ||||
|         if (!this.scrollWorking) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const target: any = e.target, | ||||
|             top = parseInt(target.scrollTop, 10), | ||||
|             goingUp = top < this.lastScroll; | ||||
|         if (goingUp) { | ||||
|             this.sideScroll.classList.remove('core-course-blocks-fixed-bottom'); | ||||
|             if (top <= this.translationY ) { | ||||
|                 // Fixed to top.
 | ||||
|                 this.translationY = top; | ||||
|                 this.sideScroll.style.transform = 'translate(0, ' + this.translationY + 'px)'; | ||||
|             } | ||||
|         } else if (top - this.translationY >= (this.blocksScrollHeight - this.vpHeight)) { | ||||
|             // Fixed to bottom.
 | ||||
|             this.sideScroll.classList.add('core-course-blocks-fixed-bottom'); | ||||
|             this.translationY = top - (this.blocksScrollHeight - this.vpHeight); | ||||
|             this.sideScroll.style.transform = 'translate(0, ' + this.translationY + 'px)'; | ||||
|         } | ||||
| 
 | ||||
|         this.lastScroll = top; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         window.removeEventListener('resize', this.initScroll); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate blocks data. | ||||
|      * | ||||
| @ -95,11 +168,13 @@ export class CoreBlockCourseBlocksComponent implements OnInit { | ||||
|                 this.element.classList.add('core-has-blocks'); | ||||
|                 this.element.classList.remove('core-no-blocks'); | ||||
| 
 | ||||
|                 this.parentContent.classList.add('core-course-block-with-blocks'); | ||||
|                 this.content.getElementRef().nativeElement.classList.add('core-course-block-with-blocks'); | ||||
| 
 | ||||
|                 this.initScroll(); | ||||
|             } else { | ||||
|                 this.element.classList.remove('core-has-blocks'); | ||||
|                 this.element.classList.add('core-no-blocks'); | ||||
|                 this.parentContent.classList.remove('core-course-block-with-blocks'); | ||||
|                 this.content.getElementRef().nativeElement.classList.remove('core-course-block-with-blocks'); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @ -6,7 +6,6 @@ | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <core-block-course-blocks [courseId]="course.id" [hideBlocks]="selectedSection && selectedSection.id == allSectionsId && canLoadMore" [downloadEnabled]="downloadEnabled"> | ||||
|     <ion-content> | ||||
|     <!-- Default course format. --> | ||||
|     <core-dynamic-component [component]="courseFormatComponent" [data]="data"> | ||||
|         <core-loading [hideUntil]="loaded"> | ||||
| @ -68,7 +67,6 @@ | ||||
|         </ion-buttons> | ||||
| 
 | ||||
|     </core-dynamic-component> | ||||
|     </ion-content> | ||||
| </core-block-course-blocks> | ||||
| 
 | ||||
| <!-- Template to render a section. --> | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| <core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled"> | ||||
|     <ion-content> | ||||
|     <core-loading [hideUntil]="dataLoaded"> | ||||
|             <ion-list> | ||||
|                 <!-- Site home main contents. --> | ||||
| @ -26,5 +25,4 @@ | ||||
| 
 | ||||
|         <core-empty-box *ngIf="!hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box> | ||||
|     </core-loading> | ||||
|     </ion-content> | ||||
| </core-block-course-blocks> | ||||
|  | ||||
| @ -702,6 +702,45 @@ export class CoreDomUtilsProvider { | ||||
|         return this.instances[id]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Wait an element to exists using the findFunction. | ||||
|      * | ||||
|      * @param {Function} findFunction The function used to find the element. | ||||
|      * @return {Promise<HTMLElement>} Resolved if found, rejected if too many tries. | ||||
|      */ | ||||
|     waitElementToExist(findFunction: Function): Promise<HTMLElement> { | ||||
|         const promiseInterval = { | ||||
|             promise: null, | ||||
|             resolve: null, | ||||
|             reject: null | ||||
|         }; | ||||
| 
 | ||||
|         let tries = 100; | ||||
| 
 | ||||
|         promiseInterval.promise = new Promise((resolve, reject): void => { | ||||
|             promiseInterval.resolve = resolve; | ||||
|             promiseInterval.reject = reject; | ||||
|         }); | ||||
| 
 | ||||
|         const clear = setInterval(() => { | ||||
|             const element: HTMLElement = findFunction(); | ||||
| 
 | ||||
|             if (element) { | ||||
|                 clearInterval(clear); | ||||
|                 promiseInterval.resolve(element); | ||||
|             } else { | ||||
|                 tries--; | ||||
| 
 | ||||
|                 if (tries <= 0) { | ||||
|                     clearInterval(clear); | ||||
|                     promiseInterval.reject(); | ||||
|                 } | ||||
|             } | ||||
|         }, 100); | ||||
| 
 | ||||
|         return promiseInterval.promise; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle bootstrap tooltips in a certain element. | ||||
|      * | ||||
|  | ||||
| @ -9,6 +9,10 @@ ion-app.app-root core-rich-text-editor .core-rte-editor { | ||||
|     margin-bottom: 1rem; | ||||
|   } | ||||
| 
 | ||||
|   .no-overflow { | ||||
|     overflow: auto; | ||||
|   } | ||||
| 
 | ||||
|   // Fix lists styles in core-format-text. | ||||
|   ul { | ||||
|     padding-left: 1rem; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user