diff --git a/src/addon/files/pages/list/list.html b/src/addon/files/pages/list/list.html index b55f063db..43dca09bd 100644 --- a/src/addon/files/pages/list/list.html +++ b/src/addon/files/pages/list/list.html @@ -3,7 +3,7 @@ - + diff --git a/src/addon/mod/data/components/index/addon-mod-data-index.html b/src/addon/mod/data/components/index/addon-mod-data-index.html index ce67bfa94..b87184715 100644 --- a/src/addon/mod/data/components/index/addon-mod-data-index.html +++ b/src/addon/mod/data/components/index/addon-mod-data-index.html @@ -95,5 +95,10 @@ {{ 'addon.mod_data.resetsettings' | translate}} - + + + + + + diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html index f7eb42ee1..8b8164d34 100644 --- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html +++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html @@ -12,7 +12,7 @@ - + @@ -25,11 +25,6 @@ 0"> - - - {{addDiscussionText}} - - @@ -97,5 +92,11 @@ + + + + + + diff --git a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html index 268a90f4d..532649f9a 100644 --- a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html @@ -25,7 +25,7 @@ - + diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d11018cb9..b63339826 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -15,7 +15,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule, COMPILER_OPTIONS } from '@angular/core'; -import { IonicApp, IonicModule, Platform, Content, ScrollEvent } from 'ionic-angular'; +import { IonicApp, IonicModule, Platform, Content, ScrollEvent, Config } from 'ionic-angular'; import { assert } from 'ionic-angular/util/util'; import { HttpModule } from '@angular/http'; import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; @@ -26,6 +26,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { MoodleMobileApp } from './app.component'; import { CoreInterceptor } from '@classes/interceptor'; +import { CorePageTransition } from '@classes/page-transition'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDbProvider } from '@providers/db'; import { CoreAppProvider } from '@providers/app'; @@ -154,7 +155,7 @@ export const CORE_PROVIDERS: any[] = [ HttpClientModule, // HttpClient is used to make JSON requests. It fails for HEAD requests because there is no content. HttpModule, IonicModule.forRoot(MoodleMobileApp, { - pageTransition: 'ios-transition' + pageTransition: 'core-page-transition' }), TranslateModule.forRoot({ loader: { @@ -257,7 +258,7 @@ export const CORE_PROVIDERS: any[] = [ ] }) export class AppModule { - constructor(platform: Platform, initDelegate: CoreInitDelegate, updateManager: CoreUpdateManagerProvider, + constructor(platform: Platform, initDelegate: CoreInitDelegate, updateManager: CoreUpdateManagerProvider, config: Config, sitesProvider: CoreSitesProvider, fileProvider: CoreFileProvider) { // Register a handler for platform ready. initDelegate.registerProcess({ @@ -289,6 +290,9 @@ export class AppModule { // Execute the init processes. initDelegate.executeInitProcesses(); + // Set transition animation. + config.setTransition('core-page-transition', CorePageTransition); + // Decorate ion-content. this.decorateIonContent(); } diff --git a/src/app/app.scss b/src/app/app.scss index 5891c94ff..1dad14d49 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -370,7 +370,13 @@ ion-col ion-select { white-space: normal; text-align: right; } +} + +.core-button-select { + ion-icon:last-child { + margin-left: 5px; } +} // File uploader. // ------------------------- @@ -677,6 +683,16 @@ canvas[core-chart] { } } +.has-fab .scroll-content{ + padding-bottom: 56px; +} + +.scroll-content ion-fab { + position: fixed; + margin-bottom: 56px; +} + + // For some reason, in iOS the pages don't inherit the background-color from ion-app, set it explicitly. .ion-page { background-color: $background-color; diff --git a/src/classes/page-transition.ts b/src/classes/page-transition.ts new file mode 100644 index 000000000..575e08c1e --- /dev/null +++ b/src/classes/page-transition.ts @@ -0,0 +1,143 @@ +// (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 { Animation } from 'ionic-angular/animations/animation'; +import { isPresent } from 'ionic-angular/util/util'; +import { PageTransition } from 'ionic-angular/transitions/page-transition'; + +const DURATION = 500; +const EASING = 'cubic-bezier(0.36,0.66,0.04,1)'; +const OPACITY = 'opacity'; +const TRANSFORM = 'transform'; +const TRANSLATEX = 'translateX'; +const CENTER = '0%'; +const OFF_OPACITY = 0.8; +const SHOW_BACK_BTN_CSS = 'show-back-button'; + +/** + * This class overrides the default transition to avoid glitches with new tabs and split view. + * Is based on IOSTransition class but it has some changes: + * - The animation is done to the full page not header, footer and content separetely. + * - On the Navbar only the back button is animated (title and other buttons will be done as a whole). Otherwise back button won't + * appear. + */ +export class CorePageTransition extends PageTransition { + init(): void { + super.init(); + const plt = this.plt; + const OFF_RIGHT = plt.isRTL ? '-99.5%' : '99.5%'; + const OFF_LEFT = plt.isRTL ? '33%' : '-33%'; + const enteringView = this.enteringView; + const leavingView = this.leavingView; + const opts = this.opts; + this.duration(isPresent(opts.duration) ? opts.duration : DURATION); + this.easing(isPresent(opts.easing) ? opts.easing : EASING); + const backDirection = (opts.direction === 'back'); + const enteringHasNavbar = (enteringView && enteringView.hasNavbar()); + const leavingHasNavbar = (leavingView && leavingView.hasNavbar()); + if (enteringView) { + // Get the native element for the entering page. + const enteringPageEle = enteringView.pageRef().nativeElement; + // Entering content. + const enteringContent = new Animation(plt, enteringPageEle); + this.add(enteringContent); + if (backDirection) { + // Entering content, back direction. + enteringContent + .fromTo(TRANSLATEX, OFF_LEFT, CENTER, true) + .fromTo(OPACITY, OFF_OPACITY, 1, true); + } + else { + // Entering content, forward direction. + enteringContent + .beforeClearStyles([OPACITY]) + .fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + } + if (enteringHasNavbar) { + // Entering page has a navbar. + const enteringNavbarEle = enteringPageEle.querySelector('ion-navbar'); + const enteringNavBar = new Animation(plt, enteringNavbarEle); + this.add(enteringNavBar); + const enteringBackButton = new Animation(plt, enteringNavbarEle.querySelector('.back-button')); + enteringNavBar + .add(enteringBackButton); + // Set properties depending on direction. + if (backDirection) { + // Entering navbar, back direction. + if (enteringView.enableBack()) { + // Back direction, entering page has a back button. + enteringBackButton + .beforeAddClass(SHOW_BACK_BTN_CSS) + .fromTo(OPACITY, 0.01, 1, true); + } + } + else { + // Entering navbar, forward direction. + if (enteringView.enableBack()) { + // Forward direction, entering page has a back button. + enteringBackButton + .beforeAddClass(SHOW_BACK_BTN_CSS) + .fromTo(OPACITY, 0.01, 1, true); + const enteringBackBtnText = new Animation(plt, enteringNavbarEle.querySelector('.back-button-text')); + enteringBackBtnText.fromTo(TRANSLATEX, (plt.isRTL ? '-100px' : '100px'), '0px'); + enteringNavBar.add(enteringBackBtnText); + } + else { + enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS); + } + } + } + } + // Setup leaving view. + if (leavingView && leavingView.pageRef()) { + // Leaving content. + const leavingPageEle = leavingView.pageRef().nativeElement; + const leavingContent = new Animation(plt, leavingPageEle); + this.add(leavingContent); + if (backDirection) { + // Leaving content, back direction. + leavingContent + .beforeClearStyles([OPACITY]) + .fromTo(TRANSLATEX, CENTER, (plt.isRTL ? '-100%' : '100%')); + } + else { + // Leaving content, forward direction. + leavingContent + .fromTo(TRANSLATEX, CENTER, OFF_LEFT) + .fromTo(OPACITY, 1, OFF_OPACITY) + .afterClearStyles([TRANSFORM, OPACITY]); + } + if (leavingHasNavbar) { + // Leaving page has a navbar. + const leavingNavbarEle = leavingPageEle.querySelector('ion-navbar'); + const leavingNavBar = new Animation(plt, leavingNavbarEle); + const leavingBackButton = new Animation(plt, leavingNavbarEle.querySelector('.back-button')); + leavingNavBar + .add(leavingBackButton); + this.add(leavingNavBar); + // Fade out leaving navbar items. + leavingBackButton.fromTo(OPACITY, 0.99, 0); + if (backDirection) { + const leavingBackBtnText = new Animation(plt, leavingNavbarEle.querySelector('.back-button-text')); + leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (plt.isRTL ? -300 : 300) + 'px'); + leavingNavBar.add(leavingBackBtnText); + } + else { + // Leaving navbar, forward direction. + leavingBackButton.afterClearStyles([OPACITY]); + } + } + } + } +} diff --git a/src/components/empty-box/empty-box.scss b/src/components/empty-box/empty-box.scss index 6c379ea4c..4f4e90575 100644 --- a/src/components/empty-box/empty-box.scss +++ b/src/components/empty-box/empty-box.scss @@ -19,6 +19,7 @@ core-empty-box { display: table-cell; text-align: center; vertical-align: middle; + pointer-events: auto; } &.core-empty-box-inline { diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html index 14b4c5dde..6a38e8470 100644 --- a/src/components/rich-text-editor/core-rich-text-editor.html +++ b/src/components/rich-text-editor/core-rich-text-editor.html @@ -5,18 +5,18 @@ - B - I - U - Normal - H1 - H2 - H3 - <pre> - - 1,2,3 - - {{ 'core.viewcode' | translate }} + + + + + + 1 + 2 + 3 + + + + {{ 'core.viewcode' | translate }} @@ -25,7 +25,7 @@ - {{ 'core.vieweditor' | translate }} + {{ 'core.vieweditor' | translate }} diff --git a/src/components/rich-text-editor/rich-text-editor.scss b/src/components/rich-text-editor/rich-text-editor.scss index bcd6e6391..53e3ddd8a 100644 --- a/src/components/rich-text-editor/rich-text-editor.scss +++ b/src/components/rich-text-editor/rich-text-editor.scss @@ -39,6 +39,11 @@ core-rich-text-editor { font-weight: bold; } + // Make empty elements selectable (to move the cursor). + *:empty:after { + content: '\200B'; + } + ul { list-style-type: disc; } diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index c082aba9b..1bfb1f0a3 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -88,6 +88,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy this.editorElement.onkeyup = this.onChange.bind(this); this.editorElement.onpaste = this.onChange.bind(this); this.editorElement.oninput = this.onChange.bind(this); + this.editorElement.onkeydown = this.moveCursor.bind(this); // Listen for changes on the control to update the editor (if it is updated from outside of this component). this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => { @@ -116,17 +117,28 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy } } + // Use paragraph on enter. + document.execCommand('DefaultParagraphSeparator', false, 'p'); + this.treatExternalContent(); this.resizeFunction = this.maximizeEditorSize.bind(this); window.addEventListener('resize', this.resizeFunction); - setTimeout(this.resizeFunction, 1000); + + let i = 0; + const interval = setInterval(() => { + const height = this.maximizeEditorSize(); + if (i >= 5 || height != 0) { + clearInterval(interval); + } + i++; + }, 750); } /** * Resize editor to maximize the space occupied. */ - protected maximizeEditorSize(): void { + protected maximizeEditorSize(): number { this.content.resize(); const contentVisibleHeight = this.content.contentHeight; @@ -138,7 +150,11 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy } else { this.element.style.height = ''; } + + return contentVisibleHeight - height; } + + return 0; } /** @@ -195,6 +211,132 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy this.contentChanged.emit(this.control.value); } + /** + * On key down function to move the cursor. + * https://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div + * + * @param {Event} $event The event. + */ + moveCursor($event: Event): void { + if (!this.rteEnabled) { + return; + } + + if ($event['key'] != 'ArrowLeft' && $event['key'] != 'ArrowRight') { + return; + } + + $event.preventDefault(); + $event.stopPropagation(); + + const move = $event['key'] == 'ArrowLeft' ? -1 : +1, + cursor = this.getCurrentCursorPosition(this.editorElement); + + this.setCurrentCursorPosition(this.editorElement, cursor + move); + } + + /** + * Returns the number of chars from the beggining where is placed the cursor. + * + * @param {Node} parent Parent where to get the position from. + * @return {number} Position in chars. + */ + protected getCurrentCursorPosition(parent: Node): number { + const selection = window.getSelection(); + + let charCount = -1, + node; + + if (selection.focusNode) { + if (parent.contains(selection.focusNode)) { + node = selection.focusNode; + charCount = selection.focusOffset; + + while (node) { + if (node.isSameNode(parent)) { + break; + } + + if (node.previousSibling) { + node = node.previousSibling; + charCount += node.textContent.length; + } else { + node = node.parentNode; + if (node === null) { + break; + } + } + } + } + } + + return charCount; + } + + /** + * Set the caret position on the character number. + * + * @param {Node} parent Parent where to set the position. + * @param {number} [chars] Number of chars where to place the caret. If not defined it will go to the end. + */ + protected setCurrentCursorPosition(parent: Node, chars?: number): void { + /** + * Loops round all the child text nodes within the supplied node and sets a range from the start of the initial node to + * the characters. + * + * @param {Node} node Node where to start. + * @param {Range} range Previous calculated range. + * @param {any} chars Object with counting of characters (input-output param). + * @return {Range} Selection range. + */ + const setRange = (node: Node, range: Range, chars: any): Range => { + if (chars.count === 0) { + range.setEnd(node, 0); + } else if (node && chars.count > 0) { + if (node.hasChildNodes()) { + // Navigate through children. + for (let lp = 0; lp < node.childNodes.length; lp++) { + range = setRange(node.childNodes[lp], range, chars); + + if (chars.count === 0) { + break; + } + } + } else if (node.textContent.length < chars.count) { + // Jump this node. + // @todo: empty nodes will be omitted. + chars.count -= node.textContent.length; + } else { + // The cursor will be placed in this element. + range.setEnd(node, chars.count); + chars.count = 0; + } + } + + return range; + }; + + let range = document.createRange(); + if (typeof chars === 'undefined') { + // Select all so it will go to the end. + range.selectNode(parent); + range.selectNodeContents(parent); + } else if (chars < 0 || chars > parent.textContent.length) { + return; + } else { + range.selectNode(parent); + range.setStart(parent, 0); + range = setRange(parent, range, {count: chars}); + } + + if (range) { + const selection = window.getSelection(); + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + } + } + /** * Toggle from rte editor to textarea syncing values. * @@ -204,7 +346,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy $event.preventDefault(); $event.stopPropagation(); - if (this.isNullOrWhiteSpace(this.control.value)) { + const isNull = this.isNullOrWhiteSpace(this.control.value); + if (isNull) { this.clearText(); } else { this.editorElement.innerHTML = this.control.value; @@ -217,14 +360,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy setTimeout(() => { if (this.rteEnabled) { this.editorElement.focus(); - - const range = document.createRange(); - range.selectNodeContents(this.editorElement); - range.collapse(false); - - const sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); + this.setCurrentCursorPosition(this.editorElement.firstChild); } else { this.textarea.setFocus(); } @@ -279,8 +415,15 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy clearText(): void { this.editorElement.innerHTML = ''; this.textarea.value = ''; + // Don't emit event so our valueChanges doesn't get notified by this change. this.control.setValue(null, {emitEvent: false}); + + setTimeout(() => { + if (this.rteEnabled) { + this.setCurrentCursorPosition(this.editorElement); + } + }, 1); } /** diff --git a/src/components/tabs/core-tabs.html b/src/components/tabs/core-tabs.html index 4fee27fb6..4045a3264 100644 --- a/src/components/tabs/core-tabs.html +++ b/src/components/tabs/core-tabs.html @@ -1,14 +1,28 @@ - - - - {{ tab.title }} - {{tab.badge}} - - + + + + + + + + + + + {{ tab.title }} + {{tab.badge}} + + + + + + + + + - \ No newline at end of file + diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index 56b3113aa..b7e7618b1 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -6,7 +6,11 @@ width: 100%; background: $core-top-tabs-background; - > a { + .row { + width: 100%; + } + + a.tab-slide { @extend .tab-button; background: $core-top-tabs-background; @@ -20,9 +24,25 @@ border-bottom: 2px solid $core-top-tabs-color-active !important; } } + + ion-col { + text-align: center; + font-size: 1.6rem; + line-height: 1.6rem; + + &.col-with-arrow { + display: flex; + justify-content: center; + align-items: center; + + ion-icon { + color: #ccc; + } + } + } } -.md .core-tabs-bar > a { +.md .core-tabs-bar a.tab-slide { // @extend .tabs-md .tab-button; min-height: $tabs-md-tab-min-height; @@ -30,17 +50,17 @@ color: $tabs-md-tab-text-color; } -.ios .core-tabs-bar > a { +.ios .core-tabs-bar a.tab-slide { // @extend .tabs-ios .tab-button; max-width: $tabs-ios-tab-max-width; min-height: $tabs-ios-tab-min-height; - font-size: $tabs-ios-tab-font-size; + font-size: $tabs-ios-tab-font-size + 4; font-weight: $tabs-ios-tab-font-weight; color: $tabs-ios-tab-text-color; } -.wp .core-tabs-bar > a { +.wp .core-tabs-bar a.tab-slide { //@extend .tabs-wp .tab-button; @include border-radius(0); diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index b86f59082..2629763ae 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -17,7 +17,7 @@ import { SimpleChange } from '@angular/core'; import { CoreTabComponent } from './tab'; -import { Content } from 'ionic-angular'; +import { Content, Slides } from 'ionic-angular'; /** * This component displays some tabs that usually share data between them. @@ -48,9 +48,16 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { @Output() ionChange: EventEmitter = new EventEmitter(); // Emitted when the tab changes. @ViewChild('originalTabs') originalTabsRef: ElementRef; @ViewChild('topTabs') topTabs: ElementRef; + @ViewChild(Slides) slides: Slides; tabs: CoreTabComponent[] = []; // List of tabs. selected: number; // Selected tab number. + showPrevButton: boolean; + showNextButton: boolean; + maxSlides = 3; + slidesShown = this.maxSlides; + numTabsShown = 0; + protected originalTabsContainer: HTMLElement; // The container of the original tabs. It will include each tab's content. protected initialized = false; protected afterViewInitTriggered = false; @@ -77,10 +84,16 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { */ ngAfterViewInit(): void { this.afterViewInitTriggered = true; + if (!this.initialized && this.hideUntil) { // Tabs should be shown, initialize them. this.initializeTabs(); } + + window.addEventListener('resize', () => { + this.calculateMaxSlides(); + this.updateSlides(); + }); } /** @@ -107,6 +120,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { if (this.getIndex(tab) == -1) { this.tabs.push(tab); this.sortTabs(); + this.updateSlides(); if (this.initialized && this.tabs.length > 1 && this.tabBarHeight == 0) { // Calculate the tabBarHeight again now that there is more than 1 tab and the bar will be seen. @@ -190,9 +204,72 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { } } + // Check which arrows should be shown + this.calculateMaxSlides(); + this.updateSlides(); + this.initialized = true; } + /** + * Method executed when the slides are changed. + */ + slideChanged(): void { + const currentIndex = this.slides.getActiveIndex(); + if (this.slidesShown >= this.numTabsShown) { + this.showPrevButton = false; + this.showNextButton = false; + } else if (typeof currentIndex !== 'undefined') { + this.showPrevButton = currentIndex > 0; + this.showNextButton = currentIndex < this.numTabsShown - this.slidesShown; + } else { + this.showPrevButton = false; + this.showNextButton = this.numTabsShown > this.slidesShown; + } + } + + /** + * Update slides. + */ + protected updateSlides(): void { + this.numTabsShown = this.tabs.reduce((prev: number, current: any) => { + return current.show ? prev + 1 : prev; + }, 0); + + this.slidesShown = Math.min(this.maxSlides, this.numTabsShown); + this.slides.update(); + this.slides.resize(); + + this.slideChanged(); + } + + protected calculateMaxSlides(): void { + if (this.slides && this.slides.renderedWidth) { + this.maxSlides = Math.floor(this.slides.renderedWidth / 120); + + return; + } + this.maxSlides = 3; + } + + /** + * Method that shows the next slide. + */ + slideNext(): void { + if (this.showNextButton) { + this.slides.slideNext(); + } + } + + /** + * Method that shows the previous slide. + */ + slidePrev(): void { + if (this.showPrevButton) { + this.slides.slidePrev(); + } + } + /** * Show or hide the tabs. This is used when the user is scrolling inside a tab. * @@ -221,6 +298,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { removeTab(tab: CoreTabComponent): void { const index = this.getIndex(tab); this.tabs.splice(index, 1); + + this.updateSlides(); } /** @@ -252,6 +331,10 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { currentTab.unselectTab(); } + if (this.selected) { + this.slides.slideTo(index); + } + this.selected = index; newTab.selectTab(); this.ionChange.emit(newTab); diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index 369c52c63..905800187 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -16,10 +16,10 @@ - - - {{section.formattedName || section.name}} - + + {{selectedSection && (selectedSection.formattedName || selectedSection.name) || 'core.course.sections' | translate }} + + diff --git a/src/core/course/components/format/format.scss b/src/core/course/components/format/format.scss index 284fe6133..e0fb24ce2 100644 --- a/src/core/course/components/format/format.scss +++ b/src/core/course/components/format/format.scss @@ -22,4 +22,8 @@ core-course-format { width: 100%; } } -} \ No newline at end of file +} + +.core-section-select { + width: 100%; +} diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 4e08d500a..9a2fd80b8 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -15,7 +15,7 @@ import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList, Injector } from '@angular/core'; -import { Content } from 'ionic-angular'; +import { Content, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; @@ -72,7 +72,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, private injector: Injector, private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, private content: Content, - prefetchDelegate: CoreCourseModulePrefetchDelegate) { + prefetchDelegate: CoreCourseModulePrefetchDelegate, private modalCtrl: ModalController) { this.selectOptions.title = translate.instant('core.course.sections'); this.completionChanged = new EventEmitter(); @@ -221,6 +221,20 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } } + /** + * Display the section selector modal. + */ + showSectionSelector(): void { + const modal = this.modalCtrl.create('CoreCourseSectionSelectorPage', + {sections: this.sections, selected: this.selectedSection}); + modal.onDidDismiss((newSection) => { + if (newSection) { + this.sectionChanged(newSection); + } + }); + modal.present(); + } + /** * Function called when selected section changes. * diff --git a/src/core/course/components/module-description/core-course-module-description.html b/src/core/course/components/module-description/core-course-module-description.html index 8ccd37fc2..412d5bf3b 100644 --- a/src/core/course/components/module-description/core-course-module-description.html +++ b/src/core/course/components/module-description/core-course-module-description.html @@ -1,6 +1,8 @@ - {{ note }} + + + {{ note }} \ No newline at end of file diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json index a672d3a15..b003ae145 100644 --- a/src/core/course/lang/en.json +++ b/src/core/course/lang/en.json @@ -11,6 +11,7 @@ "contents": "Contents", "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", "couldnotloadsections": "Could not load the sections. Please try again later.", + "coursesummary": "Course summary", "downloadcourse": "Download course", "errordownloadingcourse": "Error downloading course.", "errordownloadingsection": "Error downloading section.", diff --git a/src/core/course/pages/section-selector/section-selector.html b/src/core/course/pages/section-selector/section-selector.html new file mode 100644 index 000000000..d96876437 --- /dev/null +++ b/src/core/course/pages/section-selector/section-selector.html @@ -0,0 +1,19 @@ + + + {{ 'core.course.sections' | translate }} + + + + + + + + + + + + {{ 'core.course.nocontentavailable' | translate }} + + + + diff --git a/src/core/course/pages/section-selector/section-selector.module.ts b/src/core/course/pages/section-selector/section-selector.module.ts new file mode 100644 index 000000000..897696b62 --- /dev/null +++ b/src/core/course/pages/section-selector/section-selector.module.ts @@ -0,0 +1,33 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseSectionSelectorPage } from './section-selector'; + +@NgModule({ + declarations: [ + CoreCourseSectionSelectorPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreCourseSectionSelectorPage), + TranslateModule.forChild() + ], +}) +export class CoreCourseSectionSelectorPageModule {} diff --git a/src/core/course/pages/section-selector/section-selector.ts b/src/core/course/pages/section-selector/section-selector.ts new file mode 100644 index 000000000..0f1f23933 --- /dev/null +++ b/src/core/course/pages/section-selector/section-selector.ts @@ -0,0 +1,57 @@ +// (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 { Component } from '@angular/core'; +import { IonicPage, NavParams, ViewController } from 'ionic-angular'; +import { CoreCourseHelperProvider } from '../../providers/helper'; + +/** + * Page that displays course section selector. + */ +@IonicPage({ segment: 'core-course-section-selector' }) +@Component({ + selector: 'page-core-course-section-selector', + templateUrl: 'section-selector.html', +}) +export class CoreCourseSectionSelectorPage { + + sections: any; + selected: number; + sectionHasContent: any; + + constructor(navParams: NavParams, courseHelper: CoreCourseHelperProvider, private viewCtrl: ViewController) { + this.sections = navParams.get('sections'); + this.selected = navParams.get('selected'); + + this.sectionHasContent = courseHelper.sectionHasContent; + } + + /** + * Close the modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } + + /** + * Select a section. + * + * @param {any} section Selected section object. + */ + selectSection(section: any): void { + if (!(section.visible === 0 || section.uservisible === false)) { + this.viewCtrl.dismiss(section); + } + } +} diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index 496249479..63a0a5553 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -14,6 +14,7 @@ + diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 574165744..29aa49ac0 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -356,6 +356,13 @@ export class CoreCourseSectionPage implements OnDestroy { this.prefetchCourseData.title = statusData.title; } + /** + * Open the course summary + */ + openCourseSummary(): void { + this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: this.course, avoidOpenCourse: true}); + } + /** * Page destroyed. */ diff --git a/src/core/courses/components/course-list-item/course-list-item.ts b/src/core/courses/components/course-list-item/course-list-item.ts index 12f1bf501..d2f1efad6 100644 --- a/src/core/courses/components/course-list-item/course-list-item.ts +++ b/src/core/courses/components/course-list-item/course-list-item.ts @@ -16,6 +16,7 @@ import { Component, Input, OnInit, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreCoursesProvider } from '../../providers/courses'; +import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; /** * This directive is meant to display an item for a list of courses. @@ -32,7 +33,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit { @Input() course: any; // The course to render. constructor(@Optional() private navCtrl: NavController, private translate: TranslateService, - private coursesProvider: CoreCoursesProvider) { + private coursesProvider: CoreCoursesProvider, private courseFormatDelegate: CoreCourseFormatDelegate) { } /** @@ -80,6 +81,10 @@ export class CoreCoursesCourseListItemComponent implements OnInit { * @param {any} course The course to open. */ openCourse(course: any): void { - this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: course}); + if (course.isEnrolled) { + this.courseFormatDelegate.openCourse(this.navCtrl, course); + } else { + this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: course}); + } } } diff --git a/src/core/courses/pages/course-preview/course-preview.html b/src/core/courses/pages/course-preview/course-preview.html index 5626bbf42..1170fe30d 100644 --- a/src/core/courses/pages/course-preview/course-preview.html +++ b/src/core/courses/pages/course-preview/course-preview.html @@ -13,7 +13,7 @@ - + @@ -26,7 +26,12 @@ {{ 'core.teachers' | translate }} - {{contact.fullname}} + + + + + {{contact.fullname}} + @@ -49,7 +54,7 @@ {{ 'core.course.downloadcourse' | translate }} - + {{ 'core.course.contents' | translate }} diff --git a/src/core/courses/pages/course-preview/course-preview.ts b/src/core/courses/pages/course-preview/course-preview.ts index 7679ce57d..7189d54a5 100644 --- a/src/core/courses/pages/course-preview/course-preview.ts +++ b/src/core/courses/pages/course-preview/course-preview.ts @@ -24,6 +24,7 @@ import { CoreCoursesProvider } from '../../providers/courses'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; /** * Page that allows "previewing" a course and enrolling in it if enabled and not enrolled. @@ -41,6 +42,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { selfEnrolInstances: any[] = []; paypalEnabled: boolean; dataLoaded: boolean; + avoidOpenCourse = false; prefetchCourseData = { prefetchCourseIcon: 'spinner', title: 'core.course.downloadcourse' @@ -67,9 +69,10 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { private coursesProvider: CoreCoursesProvider, private platform: Platform, private modalCtrl: ModalController, private translate: TranslateService, private eventsProvider: CoreEventsProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate, private courseHelper: CoreCourseHelperProvider, - private courseProvider: CoreCourseProvider) { + private courseProvider: CoreCourseProvider, private courseFormatDelegate: CoreCourseFormatDelegate) { this.course = navParams.get('course'); + this.avoidOpenCourse = navParams.get('avoidOpenCourse'); this.isMobile = appProvider.isMobile(); this.isDesktop = appProvider.isDesktop(); this.downloadCourseEnabled = !this.coursesProvider.isDownloadCourseDisabledInSite(); @@ -224,11 +227,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { }).catch(() => { // The user is not an admin/manager. Check if we can provide guest access to the course. return this.canAccessAsGuest().then((passwordRequired) => { - if (!passwordRequired) { - this.canAccessCourse = true; - } else { - this.canAccessCourse = false; - } + this.canAccessCourse = !passwordRequired; }).catch(() => { this.canAccessCourse = false; }); @@ -242,12 +241,12 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { * Open the course. */ openCourse(): void { - if (!this.canAccessCourse) { - // Course cannot be opened. + if (!this.canAccessCourse || this.avoidOpenCourse) { + // Course cannot be opened or we are avoiding opening because we accessed from inside a course. return; } - this.navCtrl.push('CoreCourseSectionPage', { course: this.course }); + this.courseFormatDelegate.openCourse(this.navCtrl, this.course); } /** diff --git a/src/core/login/pages/init/init.scss b/src/core/login/pages/init/init.scss index 543fd5070..3b164ea59 100644 --- a/src/core/login/pages/init/init.scss +++ b/src/core/login/pages/init/init.scss @@ -1,10 +1,12 @@ page-core-login-init { - .core-bglogo { + .scroll-content { background-color: $core-color-init-screen; /* Change this to add a bg image or change color */ background: -webkit-radial-gradient($core-color-init-screen-alt, $core-color-init-screen); background: radial-gradient($core-color-init-screen-alt, $core-color-init-screen); background-repeat: no-repeat; background-position: center center; + } + .core-bglogo { position: absolute; top: 0; bottom: 0; diff --git a/src/core/login/pages/site/site.html b/src/core/login/pages/site/site.html index a23e975ff..6416bedce 100644 --- a/src/core/login/pages/site/site.html +++ b/src/core/login/pages/site/site.html @@ -26,9 +26,8 @@ - - - {{ 'core.login.selectsite' | translate }} + + {{ 'core.login.selectsite' | translate }} {{site.name}} @@ -37,7 +36,7 @@ {{ 'core.login.selectsite' | translate }} - {{site.name}} + {{site.name}} diff --git a/src/core/login/pages/sites/sites.html b/src/core/login/pages/sites/sites.html index 4e57f2aa5..22cae12bf 100644 --- a/src/core/login/pages/sites/sites.html +++ b/src/core/login/pages/sites/sites.html @@ -9,7 +9,7 @@ - + diff --git a/tsconfig.json b/tsconfig.json index 95c37d168..84a65ecd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,10 @@ "@components/*": ["components/*"], "@directives/*": ["directives/*"], "@pipes/*": ["pipes/*"] - } + }, + "typeRoots": [ + "node_modules/@types" + ] }, "include": [ "src/**/*.ts"
{{ 'core.login.selectsite' | translate }}