MOBILE-3103 iframe: Add fullscreen button on iframes header toolbar

main
Pau Ferrer Ocaña 2021-09-29 09:14:48 +02:00
parent be8a7b35b7
commit d8cc67eab1
9 changed files with 132 additions and 40 deletions

View File

@ -1574,6 +1574,7 @@
"core.dftimedate": "local_moodlemobileapp", "core.dftimedate": "local_moodlemobileapp",
"core.digitalminor": "moodle", "core.digitalminor": "moodle",
"core.digitalminor_desc": "moodle", "core.digitalminor_desc": "moodle",
"core.disablefullscreen": "h5p",
"core.discard": "local_moodlemobileapp", "core.discard": "local_moodlemobileapp",
"core.dismiss": "local_moodlemobileapp", "core.dismiss": "local_moodlemobileapp",
"core.displayoptions": "atto_media", "core.displayoptions": "atto_media",
@ -1657,6 +1658,7 @@
"core.forcepasswordchangenotice": "moodle", "core.forcepasswordchangenotice": "moodle",
"core.fulllistofcourses": "moodle", "core.fulllistofcourses": "moodle",
"core.fullnameandsitename": "local_moodlemobileapp", "core.fullnameandsitename": "local_moodlemobileapp",
"core.fullscreen": "h5p",
"core.grades.aggregatemean": "grades", "core.grades.aggregatemean": "grades",
"core.grades.aggregatesum": "grades", "core.grades.aggregatesum": "grades",
"core.grades.average": "grades", "core.grades.average": "grades",

View File

@ -21,7 +21,9 @@
<core-loading [hideUntil]="loaded" class="core-loading-fullheight"> <core-loading [hideUntil]="loaded" class="core-loading-fullheight">
<core-navigation-bar [previous]="previousSco" [next]="nextSco" (action)="loadSco($event)"></core-navigation-bar> <core-navigation-bar [previous]="previousSco" [next]="nextSco" (action)="loadSco($event)"></core-navigation-bar>
<core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"></core-iframe> <core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"
[showFullscreenOnToolbar]="true">
</core-iframe>
<p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p> <p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p>
</core-loading> </core-loading>

View File

@ -66,7 +66,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
protected moduleUrl!: string; // Module URL. protected moduleUrl!: string; // Module URL.
protected newAttempt = false; // Whether to start a new attempt. protected newAttempt = false; // Whether to start a new attempt.
protected organizationId?: string; // Organization ID to load. protected organizationId?: string; // Organization ID to load.
protected attempt?: number; // The attempt number. protected attempt = 0; // The attempt number.
protected offline = false; // Whether it's offline mode. protected offline = false; // Whether it's offline mode.
protected userData?: AddonModScormUserDataMap; // User data. protected userData?: AddonModScormUserDataMap; // User data.
protected initialScoId?: number; // Initial SCO ID to load. protected initialScoId?: number; // Initial SCO ID to load.
@ -96,7 +96,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
this.newAttempt = !!CoreNavigator.getRouteBooleanParam('newAttempt'); this.newAttempt = !!CoreNavigator.getRouteBooleanParam('newAttempt');
this.organizationId = CoreNavigator.getRouteParam('organizationId'); this.organizationId = CoreNavigator.getRouteParam('organizationId');
this.initialScoId = CoreNavigator.getRouteNumberParam('scoId'); this.initialScoId = CoreNavigator.getRouteNumberParam('scoId');
this.siteId = CoreSites.getCurrentSiteId(); this.siteId = CoreSites.getRequiredCurrentSite().getId();
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModal(error); CoreDomUtils.showErrorModal(error);
@ -150,14 +150,12 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
this.showToc = AddonModScorm.displayTocInPlayer(this.scorm); this.showToc = AddonModScorm.displayTocInPlayer(this.scorm);
if (this.scorm.popup) { if (this.scorm.popup) {
this.mainMenuPage.changeVisibility(false);
// If we receive a value > 100 we assume it's a fixed pixel size. // If we receive a value > 100 we assume it's a fixed pixel size.
if (this.scorm.width! > 100) { if (this.scorm.width && this.scorm.width > 100) {
this.scormWidth = this.scorm.width; this.scormWidth = this.scorm.width;
// Only get fixed size on height if width is also fixed. // Only get fixed size on height if width is also fixed.
if (this.scorm.height! > 100) { if (this.scorm.height && this.scorm.height > 100) {
this.scormHeight = this.scorm.height; this.scormHeight = this.scorm.height;
} }
} }
@ -198,7 +196,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
// Wait a bit to prevent collisions between this store and SCORM API's store. // Wait a bit to prevent collisions between this store and SCORM API's store.
setTimeout(async () => { setTimeout(async () => {
try { try {
AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt!); AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt);
} catch (error) { } catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true); CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
} }
@ -292,7 +290,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
await this.determineAttemptAndMode(attemptsData); await this.determineAttemptAndMode(attemptsData);
const [data, accessInfo] = await Promise.all([ const [data, accessInfo] = await Promise.all([
AddonModScorm.getScormUserData(this.scorm.id, this.attempt!, { AddonModScorm.getScormUserData(this.scorm.id, this.attempt, {
cmId: this.cmId, cmId: this.cmId,
offline: this.offline, offline: this.offline,
}), }),
@ -319,13 +317,13 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
try { try {
// We need to check incomplete again: attempt number or status might have changed. // We need to check incomplete again: attempt number or status might have changed.
this.incomplete = await AddonModScorm.isAttemptIncomplete(this.scorm.id, this.attempt!, { this.incomplete = await AddonModScorm.isAttemptIncomplete(this.scorm.id, this.attempt, {
offline: this.offline, offline: this.offline,
cmId: this.cmId, cmId: this.cmId,
}); });
// Get TOC. // Get TOC.
this.toc = await AddonModScormHelper.getToc(this.scorm.id, this.attempt!, this.incomplete, { this.toc = await AddonModScormHelper.getToc(this.scorm.id, this.attempt, this.incomplete, {
organization: this.organizationId, organization: this.organizationId,
offline: this.offline, offline: this.offline,
cmId: this.cmId, cmId: this.cmId,
@ -351,7 +349,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
} }
// No SCO defined. Get the first valid one. // No SCO defined. Get the first valid one.
const sco = await AddonModScormHelper.getFirstSco(this.scorm.id, this.attempt!, { const sco = await AddonModScormHelper.getFirstSco(this.scorm.id, this.attempt, {
toc: this.toc, toc: this.toc,
organization: this.organizationId, organization: this.organizationId,
mode: this.mode, mode: this.mode,
@ -383,7 +381,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
this.siteId, this.siteId,
this.scorm, this.scorm,
sco.id, sco.id,
this.attempt!, this.attempt,
this.userData!, this.userData!,
this.mode, this.mode,
this.offline, this.offline,
@ -446,14 +444,14 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
}]; }];
try { try {
AddonModScorm.saveTracks(sco.id, this.attempt!, tracks, this.scorm, this.offline); AddonModScorm.saveTracks(sco.id, this.attempt, tracks, this.scorm, this.offline);
} catch { } catch {
// Error saving data. Go offline if needed. // Error saving data. Go offline if needed.
if (this.offline) { if (this.offline) {
return; return;
} }
const data = await AddonModScorm.getScormUserData(this.scorm.id, this.attempt!, { const data = await AddonModScorm.getScormUserData(this.scorm.id, this.attempt, {
cmId: this.cmId, cmId: this.cmId,
}); });
@ -464,12 +462,12 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
try { try {
// Go offline. // Go offline.
await AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt!); await AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt);
this.offline = true; this.offline = true;
this.dataModel?.setOffline(true); this.dataModel?.setOffline(true);
await AddonModScorm.saveTracks(sco.id, this.attempt!, tracks, this.scorm, true); await AddonModScorm.saveTracks(sco.id, this.attempt, tracks, this.scorm, true);
} catch (error) { } catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true); CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
} }
@ -528,7 +526,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
value: String(CoreTimeUtils.timestamp()), value: String(CoreTimeUtils.timestamp()),
}]; }];
await AddonModScorm.saveTracks(scoId, this.attempt!, tracks, this.scorm, this.offline); await AddonModScorm.saveTracks(scoId, this.attempt, tracks, this.scorm, this.offline);
if (this.offline) { if (this.offline) {
return; return;
@ -541,22 +539,6 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
})); }));
} }
/**
* @inheritdoc
*/
ionViewDidEnter(): void {
if (this.scorm && this.scorm.popup) {
this.mainMenuPage.changeVisibility(false);
}
}
/**
* @inheritdoc
*/
ionViewWillLeave(): void {
this.mainMenuPage.changeVisibility(true);
}
/** /**
* Component being destroyed. * Component being destroyed.
*/ */

View File

@ -1,4 +1,13 @@
<div [class.core-loading-container]="loading || !safeUrl" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}"> <div [class.core-loading-container]="loading || !safeUrl" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}">
<core-navbar-buttons slot="end" append *ngIf="initialized && showFullscreenOnToolbar">
<ion-button fill="clear" (click)="toggleFullscreen()"
[attr.aria-label]="(fullscreen ? 'core.disablefullscreen' : 'core.fullscreen') | translate" >
<ion-icon *ngIf="!fullscreen" name="fas-expand" slot="icon-only" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="fullscreen" name="fas-compress" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-navbar-buttons>
<!-- Don't add the iframe until safeUrl is set, adding an iframe with null as src causes the iframe to load the whole app. --> <!-- Don't add the iframe until safeUrl is set, adding an iframe with null as src causes the iframe to load the whole app. -->
<iframe #iframe *ngIf="safeUrl" [hidden]="loading" class="core-iframe" <iframe #iframe *ngIf="safeUrl" [hidden]="loading" class="core-iframe"
[ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl"

View File

@ -30,3 +30,14 @@
} }
} }
} }
:host-context(.core-iframe-fullscreen) {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
width: 100%;
height: 100%;
z-index: 9999;
}

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { import {
Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, OnDestroy,
} from '@angular/core'; } from '@angular/core';
import { SafeResourceUrl } from '@angular/platform-browser'; import { SafeResourceUrl } from '@angular/platform-browser';
@ -22,7 +22,6 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { CoreIframeUtils } from '@services/utils/iframe'; import { CoreIframeUtils } from '@services/utils/iframe';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from '@singletons/logger';
import { DomSanitizer } from '@singletons'; import { DomSanitizer } from '@singletons';
@Component({ @Component({
@ -30,7 +29,7 @@ import { DomSanitizer } from '@singletons';
templateUrl: 'core-iframe.html', templateUrl: 'core-iframe.html',
styleUrls: ['iframe.scss'], styleUrls: ['iframe.scss'],
}) })
export class CoreIframeComponent implements OnChanges { export class CoreIframeComponent implements OnChanges, OnDestroy {
static loadingTimeout = 15000; static loadingTimeout = 15000;
@ -39,17 +38,19 @@ export class CoreIframeComponent implements OnChanges {
@Input() iframeWidth?: string; @Input() iframeWidth?: string;
@Input() iframeHeight?: string; @Input() iframeHeight?: string;
@Input() allowFullscreen?: boolean | string; @Input() allowFullscreen?: boolean | string;
@Input() showFullscreenOnToolbar?: boolean | string;
@Output() loaded: EventEmitter<HTMLIFrameElement> = new EventEmitter<HTMLIFrameElement>(); @Output() loaded: EventEmitter<HTMLIFrameElement> = new EventEmitter<HTMLIFrameElement>();
loading?: boolean; loading?: boolean;
safeUrl?: SafeResourceUrl; safeUrl?: SafeResourceUrl;
displayHelp = false; displayHelp = false;
fullscreen = false;
protected logger: CoreLogger; initialized = false;
protected initialized = false;
protected style?: HTMLStyleElement;
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreIframe');
this.loaded = new EventEmitter<HTMLIFrameElement>(); this.loaded = new EventEmitter<HTMLIFrameElement>();
} }
@ -71,6 +72,16 @@ export class CoreIframeComponent implements OnChanges {
this.iframeWidth = (this.iframeWidth && CoreDomUtils.formatPixelsSize(this.iframeWidth)) || '100%'; this.iframeWidth = (this.iframeWidth && CoreDomUtils.formatPixelsSize(this.iframeWidth)) || '100%';
this.iframeHeight = (this.iframeHeight && CoreDomUtils.formatPixelsSize(this.iframeHeight)) || '100%'; this.iframeHeight = (this.iframeHeight && CoreDomUtils.formatPixelsSize(this.iframeHeight)) || '100%';
this.allowFullscreen = CoreUtils.isTrueOrOne(this.allowFullscreen); this.allowFullscreen = CoreUtils.isTrueOrOne(this.allowFullscreen);
this.showFullscreenOnToolbar = CoreUtils.isTrueOrOne(this.showFullscreenOnToolbar);
if (this.showFullscreenOnToolbar) {
const shadow =
iframe.closest('.ion-page')?.querySelector('ion-header ion-toolbar')?.shadowRoot;
if (shadow) {
this.style = document.createElement('style');
shadow.appendChild(this.style);
}
}
// Show loading only with external URLs. // Show loading only with external URLs.
this.loading = !this.src || !CoreUrlUtils.isLocalFileUrl(this.src); this.loading = !this.src || !CoreUrlUtils.isLocalFileUrl(this.src);
@ -120,4 +131,32 @@ export class CoreIframeComponent implements OnChanges {
CoreIframeUtils.openIframeHelpModal(); CoreIframeUtils.openIframeHelpModal();
} }
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.toggleFullscreen(false);
}
/**
* Toggle fullscreen mode.
*/
toggleFullscreen(enable?: boolean): void {
if (enable !== undefined) {
this.fullscreen = enable;
} else {
this.fullscreen = !this.fullscreen;
}
if (this.style) {
// Done this way because of the shadow DOM.
this.style.textContent = this.fullscreen
? '@media screen and (orientation: landscape) {\
.toolbar-container { flex-direction: column-reverse !important; height: 100%; } }'
: '';
}
document.body.classList.toggle('core-iframe-fullscreen', this.fullscreen);
}
} }

View File

@ -82,6 +82,7 @@
"dftimedate": "h[:]mm A", "dftimedate": "h[:]mm A",
"digitalminor": "Digital minor", "digitalminor": "Digital minor",
"digitalminor_desc": "Please ask your parent/guardian to contact:", "digitalminor_desc": "Please ask your parent/guardian to contact:",
"disablefullscreen": "Disable fullscreen",
"discard": "Discard", "discard": "Discard",
"dismiss": "Dismiss", "dismiss": "Dismiss",
"displayoptions": "Display options", "displayoptions": "Display options",
@ -122,6 +123,7 @@
"forcepasswordchangenotice": "You must change your password to proceed.", "forcepasswordchangenotice": "You must change your password to proceed.",
"fulllistofcourses": "All courses", "fulllistofcourses": "All courses",
"fullnameandsitename": "{{fullname}} ({{sitename}})", "fullnameandsitename": "{{fullname}} ({{sitename}})",
"fullscreen": "Fullscreen",
"group": "Group", "group": "Group",
"groupsseparate": "Separate groups", "groupsseparate": "Separate groups",
"groupsvisible": "Visible groups", "groupsvisible": "Visible groups",

View File

@ -384,6 +384,46 @@ ion-toolbar {
} }
} }
// Iframe fullscreen manage.
// Using router outlet to avoid changing styles on modals.
body.core-iframe-fullscreen ion-router-outlet {
ion-tab-bar.mainmenu-tabs {
display: none;
}
--core-header-toolbar-height: 48px;
--core-header-toolbar-color: white;
--core-header-toolbar-background: black;
--core-header-toolbar-border-width: 0;
ion-header ion-toolbar {
h1, ion-back-button {
display: none;
}
}
@media screen and (orientation: landscape) {
// Place ion-header on the side and hide text
.ion-page {
flex-direction: row-reverse;
ion-header {
width: var(--core-header-toolbar-height);
ion-toolbar {
height: 100%;
--padding-start: 0;
--padding-end: 0;
}
ion-buttons {
flex-direction: column-reverse;
}
}
}
}
}
// Modals. // Modals.
.core-modal-fullscreen .modal-wrapper { .core-modal-fullscreen .modal-wrapper {
position: absolute; position: absolute;

View File

@ -236,6 +236,11 @@
--color: var(--subdued-text-color); --color: var(--subdued-text-color);
} }
ion-back-button {
--min-height: var(--a11y-min-target-size);
--min-width: var(--a11y-min-target-size);
}
--core-combobox-background: var(--ion-item-background); --core-combobox-background: var(--ion-item-background);
--core-combobox-color: var(--black); --core-combobox-color: var(--black);
--core-combobox-border-color: var(--primary); --core-combobox-border-color: var(--primary);