diff --git a/src/components/ion-tabs/ion-tabs.scss b/src/components/ion-tabs/ion-tabs.scss index fabba1828..20875d511 100644 --- a/src/components/ion-tabs/ion-tabs.scss +++ b/src/components/ion-tabs/ion-tabs.scss @@ -23,6 +23,12 @@ ion-app.app-root core-ion-tabs { background-color: $ion-tabs-badge-color; } + &[tabsplacement="bottom"] { + .ion-page > ion-content > .scroll-content { + margin-bottom: $navbar-md-height !important; + } + } + &[tabsplacement="side"] { .tabbar { @include float(start); @@ -53,6 +59,10 @@ ion-app.app-root core-ion-tabs { position: relative; } } + + .scroll-content, .fixed-content { + margin-bottom: 0 !important; + } } } diff --git a/src/core/block/block.module.ts b/src/core/block/block.module.ts index 764d27fb9..6361d2c2d 100644 --- a/src/core/block/block.module.ts +++ b/src/core/block/block.module.ts @@ -14,9 +14,8 @@ import { NgModule } from '@angular/core'; import { CoreBlockDelegate } from './providers/delegate'; +import { CoreBlockHelperProvider } from './providers/helper'; import { CoreBlockDefaultHandler } from './providers/default-block-handler'; -import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; -import { CoreBlockCourseBlocksCourseOptionHandler } from './providers/course-option-handler'; import { CoreBlockComponentsModule } from './components/components.module'; // List of providers (without handlers). @@ -31,14 +30,10 @@ export const CORE_BLOCK_PROVIDERS: any[] = [ ], providers: [ CoreBlockDelegate, - CoreBlockDefaultHandler, - CoreBlockCourseBlocksCourseOptionHandler + CoreBlockHelperProvider, + CoreBlockDefaultHandler ], exports: [] }) export class CoreBlockModule { - constructor(courseOptionHandler: CoreBlockCourseBlocksCourseOptionHandler, - courseOptionsDelegate: CoreCourseOptionsDelegate) { - courseOptionsDelegate.registerHandler(courseOptionHandler); - } } diff --git a/src/core/block/components/course-blocks/core-block-course-blocks.html b/src/core/block/components/course-blocks/core-block-course-blocks.html index cf9457d58..be7bb0c1d 100644 --- a/src/core/block/components/course-blocks/core-block-course-blocks.html +++ b/src/core/block/components/course-blocks/core-block-course-blocks.html @@ -1,15 +1,14 @@ - - - - +
+ +
+ + - + - + - - diff --git a/src/core/block/components/course-blocks/course-blocks.scss b/src/core/block/components/course-blocks/course-blocks.scss new file mode 100644 index 000000000..f4a9df06a --- /dev/null +++ b/src/core/block/components/course-blocks/course-blocks.scss @@ -0,0 +1,74 @@ +$core-side-blocks-max-width: 320px; +$core-side-blocks-min-width: 30%; + +.core-course-block-with-blocks > .scroll-content { + overflow-y: visible; +} + +ion-app.app-root core-block-course-blocks { + + &.core-no-blocks { + .core-course-blocks-content > ion-content { + height: auto; + contain: content; + + > .scroll-content { + overflow-y: visible; + position: relative; + contain: content; + } + } + } + + &.core-has-blocks { + @include media-breakpoint-up(md) { + @include position(0, 0, 0, 0); + + position: absolute; + + display: flex; + + flex-direction: row; + flex-wrap: nowrap; + + contain: strict; + + .core-course-blocks-content { + min-width: calc(100% - #{($core-side-blocks-max-width)}); + max-width: calc(100% - #{($core-side-blocks-min-width)}); + z-index: 0; + flex: 1; + box-shadow: none !important; + } + + 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-width; + min-width: $core-side-blocks-min-width; + @include border-start(1px, solid, $list-md-border-color); + } + } + + @include media-breakpoint-down(sm) { + // Disable scroll on individual columns. + .core-course-blocks-content > ion-content, + ion-content.core-course-blocks-side { + height: auto; + contain: content; + + &.core-hide-blocks { + display: none; + } + + > .scroll-content { + overflow-y: visible; + position: relative; + contain: content; + } + } + } + } +} diff --git a/src/core/block/components/course-blocks/course-blocks.ts b/src/core/block/components/course-blocks/course-blocks.ts index 8adbb5e4e..744dd4326 100644 --- a/src/core/block/components/course-blocks/course-blocks.ts +++ b/src/core/block/components/course-blocks/course-blocks.ts @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChildren, Input, OnInit, QueryList } from '@angular/core'; +import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef, Optional } from '@angular/core'; +import { Content } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreBlockComponent } from '../block/block'; -import { CoreBlockDelegate } from '../../providers/delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreBlockComponent } from '../block/block'; +import { CoreBlockHelperProvider } from '../../providers/helper'; /** * Component that displays the list of course blocks. @@ -28,16 +29,22 @@ import { CoreCourseProvider } from '@core/course/providers/course'; export class CoreBlockCourseBlocksComponent implements OnInit { @Input() courseId: number; + @Input() hideBlocks = false; + @Input() downloadEnabled: boolean; @ViewChildren(CoreBlockComponent) blocksComponents: QueryList; dataLoaded = false; - hasContent: boolean; - hasSupportedBlock: boolean; blocks = []; + protected element: HTMLElement; + protected parentContent: HTMLElement; + constructor(private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, - private blockDelegate: CoreBlockDelegate) { + protected blockHelper: CoreBlockHelperProvider, element: ElementRef, + @Optional() content: Content) { + this.element = element.nativeElement; + this.parentContent = content.getElementRef().nativeElement; } /** @@ -50,14 +57,14 @@ export class CoreBlockCourseBlocksComponent implements OnInit { } /** - * Refresh the data. + * Invalidate blocks data. * - * @param {any} refresher Refresher. + * @return {Promise} Promise resolved when done. */ - doRefresh(refresher: any): void { + invalidateBlocks(): Promise { const promises = []; - if (this.courseProvider.canGetCourseBlocks()) { + if (this.blockHelper.canGetCourseBlocks()) { promises.push(this.courseProvider.invalidateCourseBlocks(this.courseId)); } @@ -68,11 +75,7 @@ export class CoreBlockCourseBlocksComponent implements OnInit { })); }); - Promise.all(promises).finally(() => { - this.loadContent().finally(() => { - refresher.complete(); - }); - }); + return Promise.all(promises); } /** @@ -80,21 +83,24 @@ export class CoreBlockCourseBlocksComponent implements OnInit { * * @return {Promise} Promise resolved when done. */ - protected loadContent(): Promise { - // Get site home blocks. - const canGetBlocks = this.courseProvider.canGetCourseBlocks(), - promise = canGetBlocks ? this.courseProvider.getCourseBlocks(this.courseId) : Promise.reject(null); - - return promise.then((blocks) => { + loadContent(): Promise { + return this.blockHelper.getCourseBlocks(this.courseId).then((blocks) => { this.blocks = blocks; - this.hasSupportedBlock = this.blockDelegate.hasSupportedBlock(blocks); - }).catch((error) => { - if (canGetBlocks) { - this.domUtils.showErrorModal(error); - } - this.blocks = []; - }); + this.domUtils.showErrorModal(error); + this.blocks = []; + }).finally(() => { + if (this.blocks.length > 0) { + this.element.classList.add('core-has-blocks'); + this.element.classList.remove('core-no-blocks'); + + this.parentContent.classList.add('core-course-block-with-blocks'); + } else { + this.element.classList.remove('core-has-blocks'); + this.element.classList.add('core-no-blocks'); + this.parentContent.classList.remove('core-course-block-with-blocks'); + } + }); } } diff --git a/src/core/block/components/only-title-block/core-block-only-title.html b/src/core/block/components/only-title-block/core-block-only-title.html index 287592371..358fbde44 100644 --- a/src/core/block/components/only-title-block/core-block-only-title.html +++ b/src/core/block/components/only-title-block/core-block-only-title.html @@ -1,3 +1,3 @@ -

{{ title | translate }}

+

\ No newline at end of file diff --git a/src/core/block/providers/course-option-handler.ts b/src/core/block/providers/course-option-handler.ts deleted file mode 100644 index 893904ed8..000000000 --- a/src/core/block/providers/course-option-handler.ts +++ /dev/null @@ -1,91 +0,0 @@ -// (C) Copyright 2015 Martin Dougiamas -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Injectable, Injector } from '@angular/core'; -import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreBlockCourseBlocksComponent } from '../components/course-blocks/course-blocks'; -import { CoreBlockDelegate } from './delegate'; - -/** - * Course nav handler. - */ -@Injectable() -export class CoreBlockCourseBlocksCourseOptionHandler implements CoreCourseOptionsHandler { - name = 'CoreCourseBlocks'; - priority = 700; - - constructor(private courseProvider: CoreCourseProvider, private blockDelegate: CoreBlockDelegate) {} - - /** - * Should invalidate the data to determine if the handler is enabled for a certain course. - * - * @param {number} courseId The course ID. - * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. - * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. - * @return {Promise} Promise resolved when done. - */ - invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise { - return this.courseProvider.invalidateCourseBlocks(courseId); - } - - /** - * Check if the handler is enabled on a site level. - * - * @return {boolean} Whether or not the handler is enabled on a site level. - */ - isEnabled(): boolean | Promise { - return this.courseProvider.canGetCourseBlocks() && !this.blockDelegate.areBlocksDisabledInCourses(); - } - - /** - * Whether or not the handler is enabled for a certain course. - * - * @param {number} courseId The course ID. - * @param {any} accessData Access type and data. Default, guest, ... - * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. - * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. - * @return {boolean|Promise} True or promise resolved with true if enabled. - */ - isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { - return this.courseProvider.getCourseBlocks(courseId).then((blocks) => { - return blocks && blocks.length > 0; - }); - } - - /** - * Returns the data needed to render the handler. - * - * @param {Injector} injector Injector. - * @param {number} courseId The course ID. - * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. - */ - getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { - return { - title: 'core.block.blocks', - class: 'core-course-blocks-handler', - component: CoreBlockCourseBlocksComponent - }; - } - - /** - * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. - * - * @param {any} course The course. - * @return {Promise} Promise resolved when done. - */ - prefetch(course: any): Promise { - return this.courseProvider.getCourseBlocks(course.id); - } -} diff --git a/src/core/block/providers/helper.ts b/src/core/block/providers/helper.ts new file mode 100644 index 000000000..14473f757 --- /dev/null +++ b/src/core/block/providers/helper.ts @@ -0,0 +1,59 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; + +/** + * Service that provides helper functions for blocks. + */ +@Injectable() +export class CoreBlockHelperProvider { + + constructor(protected courseProvider: CoreCourseProvider, protected blockDelegate: CoreBlockDelegate) {} + + /** + * Return if it get course blocks options is enabled for the current site. + * + * @return {boolean} true if enabled, false otherwise. + */ + canGetCourseBlocks(): boolean { + return this.courseProvider.canGetCourseBlocks() && !this.blockDelegate.areBlocksDisabledInCourses(); + } + + /** + * Returns the list of blocks for the selected course. + * + * @param {number} courseId Course ID. + * @return {Promise} List of supported blocks. + */ + getCourseBlocks(courseId: number): Promise { + const canGetBlocks = this.canGetCourseBlocks(); + + if (!canGetBlocks) { + return Promise.resolve([]); + } + + return this.courseProvider.getCourseBlocks(courseId).then((blocks) => { + const hasSupportedBlock = this.blockDelegate.hasSupportedBlock(blocks); + + if (!hasSupportedBlock) { + return []; + } + + return blocks; + }); + } +} diff --git a/src/core/course/components/components.module.ts b/src/core/course/components/components.module.ts index a56f920d8..750433ab4 100644 --- a/src/core/course/components/components.module.ts +++ b/src/core/course/components/components.module.ts @@ -18,6 +18,7 @@ import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreBlockComponentsModule } from '@core/block/components/components.module'; import { CoreCourseFormatComponent } from './format/format'; import { CoreCourseModuleComponent } from './module/module'; import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; @@ -33,6 +34,7 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup CoreCourseUnsupportedModuleComponent ], imports: [ + CoreBlockComponentsModule, CommonModule, IonicModule, TranslateModule.forChild(), diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index 9ba99a96c..02be4858e 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -5,67 +5,71 @@ - - - - - -
- - - -
-
+ + + + + + + +
+ + + +
+
- - - -
- + + + +
+ +
+ + + +
+
+ + +
+ + + +
- - - - + + +
+ + + + + + + + + +
+ + + + + + - - -
- - - - -
- - -
- - - - - - - - - -
- - - - - - - + + diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 8b419c87c..cf2744238 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -13,7 +13,7 @@ // limitations under the License. import { - Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList, Injector + Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList, Injector, ViewChild } from '@angular/core'; import { Content, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; @@ -24,6 +24,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreBlockCourseBlocksComponent } from '@core/block/components/course-blocks/course-blocks'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; /** @@ -52,6 +53,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Output() completionChanged?: EventEmitter; // Will emit an event when any module completion changes. @ViewChildren(CoreDynamicComponent) dynamicComponents: QueryList; + @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent; // All the possible component classes. courseFormatComponent: any; @@ -420,6 +422,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { promises.push(Promise.resolve(component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]))); }); + promises.push(this.courseBlocksComponent.invalidateBlocks().finally(() => { + return this.courseBlocksComponent.loadContent(); + })); + return Promise.all(promises); } diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index efbd0d748..d787aae14 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -20,6 +20,8 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTabsComponent } from '@components/tabs/tabs'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCourseProvider } from '../../providers/course'; import { CoreCourseHelperProvider } from '../../providers/helper'; import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; @@ -28,8 +30,6 @@ import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay, CoreCourseOptionsMenuHandlerToDisplay } from '../../providers/options-delegate'; import { CoreCourseSyncProvider } from '../../providers/sync'; import { CoreCourseFormatComponent } from '../../components/format/format'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; -import { CoreTabsComponent } from '@components/tabs/tabs'; /** * Page that displays the list of courses the user is enrolled in. diff --git a/src/core/sitehome/components/index/core-sitehome-index.html b/src/core/sitehome/components/index/core-sitehome-index.html index 17a52f8b0..389a968ac 100644 --- a/src/core/sitehome/components/index/core-sitehome-index.html +++ b/src/core/sitehome/components/index/core-sitehome-index.html @@ -1,32 +1,30 @@ - + + + + + + + + + - - - - - - + + - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/core/sitehome/components/index/index.ts b/src/core/sitehome/components/index/index.ts index a629f76c4..8c8a511c1 100644 --- a/src/core/sitehome/components/index/index.ts +++ b/src/core/sitehome/components/index/index.ts @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChildren, QueryList, Input } from '@angular/core'; +import { Component, OnInit, Input, ViewChild } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; -import { CoreBlockDelegate } from '@core/block/providers/delegate'; -import { CoreBlockComponent } from '@core/block/components/block/block'; +import { CoreBlockCourseBlocksComponent } from '@core/block/components/course-blocks/course-blocks'; import { CoreSite } from '@classes/site'; /** @@ -30,21 +29,19 @@ import { CoreSite } from '@classes/site'; templateUrl: 'core-sitehome-index.html', }) export class CoreSiteHomeIndexComponent implements OnInit { - @ViewChildren(CoreBlockComponent) blocksComponents: QueryList; @Input() downloadEnabled: boolean; + @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent; dataLoaded = false; section: any; hasContent: boolean; - hasSupportedBlock: boolean; items: any[] = []; siteHomeId: number; currentSite: CoreSite; - blocks = []; constructor(private domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, private courseHelper: CoreCourseHelperProvider, - private prefetchDelegate: CoreCourseModulePrefetchDelegate, private blockDelegate: CoreBlockDelegate) { + private prefetchDelegate: CoreCourseModulePrefetchDelegate) { this.currentSite = sitesProvider.getCurrentSite(); this.siteHomeId = this.currentSite.getSiteHomeId(); } @@ -79,19 +76,15 @@ export class CoreSiteHomeIndexComponent implements OnInit { promises.push(this.prefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); } - if (this.courseProvider.canGetCourseBlocks()) { - promises.push(this.courseProvider.invalidateCourseBlocks(this.siteHomeId)); - } - - // Invalidate the blocks. - this.blocksComponents.forEach((blockComponent) => { - promises.push(blockComponent.invalidate().catch(() => { - // Ignore errors. - })); - }); + promises.push(this.courseBlocksComponent.invalidateBlocks()); Promise.all(promises).finally(() => { - this.loadContent().finally(() => { + const p2 = []; + + p2.push(this.loadContent()); + p2.push(this.courseBlocksComponent.loadContent()); + + return Promise.all(p2).finally(() => { refresher.complete(); }); }); @@ -149,32 +142,6 @@ export class CoreSiteHomeIndexComponent implements OnInit { this.currentSite && this.currentSite.getInfo().sitename).catch(() => { // Ignore errors. }); - - // Get site home blocks. - const canGetBlocks = this.courseProvider.canGetCourseBlocks(), - promise = canGetBlocks ? this.courseProvider.getCourseBlocks(this.siteHomeId) : Promise.reject(null); - - return promise.then((blocks) => { - this.blocks = blocks; - this.hasSupportedBlock = this.blockDelegate.hasSupportedBlock(blocks); - - }).catch((error) => { - if (canGetBlocks) { - this.domUtils.showErrorModal(error); - } - this.blocks = []; - - // Cannot get the blocks, just show site main menu if needed. - const section = sections.find((section) => section.section == 0); - if (section && this.courseHelper.sectionHasContent(section)) { - this.blocks.push({ - name: 'site_main_menu' - }); - this.hasSupportedBlock = true; - } else { - this.hasSupportedBlock = false; - } - }); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true); });