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.digitalminor": "moodle",
"core.digitalminor_desc": "moodle",
"core.disablefullscreen": "h5p",
"core.discard": "local_moodlemobileapp",
"core.dismiss": "local_moodlemobileapp",
"core.displayoptions": "atto_media",
@ -1657,6 +1658,7 @@
"core.forcepasswordchangenotice": "moodle",
"core.fulllistofcourses": "moodle",
"core.fullnameandsitename": "local_moodlemobileapp",
"core.fullscreen": "h5p",
"core.grades.aggregatemean": "grades",
"core.grades.aggregatesum": "grades",
"core.grades.average": "grades",

View File

@ -21,7 +21,9 @@
<core-loading [hideUntil]="loaded" class="core-loading-fullheight">
<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>
</core-loading>

View File

@ -66,7 +66,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
protected moduleUrl!: string; // Module URL.
protected newAttempt = false; // Whether to start a new attempt.
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 userData?: AddonModScormUserDataMap; // User data.
protected initialScoId?: number; // Initial SCO ID to load.
@ -96,7 +96,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
this.newAttempt = !!CoreNavigator.getRouteBooleanParam('newAttempt');
this.organizationId = CoreNavigator.getRouteParam('organizationId');
this.initialScoId = CoreNavigator.getRouteNumberParam('scoId');
this.siteId = CoreSites.getCurrentSiteId();
this.siteId = CoreSites.getRequiredCurrentSite().getId();
} catch (error) {
CoreDomUtils.showErrorModal(error);
@ -150,14 +150,12 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
this.showToc = AddonModScorm.displayTocInPlayer(this.scorm);
if (this.scorm.popup) {
this.mainMenuPage.changeVisibility(false);
// 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;
// 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;
}
}
@ -198,7 +196,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
// Wait a bit to prevent collisions between this store and SCORM API's store.
setTimeout(async () => {
try {
AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt!);
AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
}
@ -292,7 +290,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
await this.determineAttemptAndMode(attemptsData);
const [data, accessInfo] = await Promise.all([
AddonModScorm.getScormUserData(this.scorm.id, this.attempt!, {
AddonModScorm.getScormUserData(this.scorm.id, this.attempt, {
cmId: this.cmId,
offline: this.offline,
}),
@ -319,13 +317,13 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
try {
// 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,
cmId: this.cmId,
});
// 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,
offline: this.offline,
cmId: this.cmId,
@ -351,7 +349,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
}
// 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,
organization: this.organizationId,
mode: this.mode,
@ -383,7 +381,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
this.siteId,
this.scorm,
sco.id,
this.attempt!,
this.attempt,
this.userData!,
this.mode,
this.offline,
@ -446,14 +444,14 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
}];
try {
AddonModScorm.saveTracks(sco.id, this.attempt!, tracks, this.scorm, this.offline);
AddonModScorm.saveTracks(sco.id, this.attempt, tracks, this.scorm, this.offline);
} catch {
// Error saving data. Go offline if needed.
if (this.offline) {
return;
}
const data = await AddonModScorm.getScormUserData(this.scorm.id, this.attempt!, {
const data = await AddonModScorm.getScormUserData(this.scorm.id, this.attempt, {
cmId: this.cmId,
});
@ -464,12 +462,12 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
try {
// Go offline.
await AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt!);
await AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt);
this.offline = 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) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
}
@ -528,7 +526,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
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) {
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.
*/

View File

@ -1,4 +1,13 @@
<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. -->
<iframe #iframe *ngIf="safeUrl" [hidden]="loading" class="core-iframe"
[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.
import {
Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange,
Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, OnDestroy,
} from '@angular/core';
import { SafeResourceUrl } from '@angular/platform-browser';
@ -22,7 +22,6 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreIframeUtils } from '@services/utils/iframe';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from '@singletons/logger';
import { DomSanitizer } from '@singletons';
@Component({
@ -30,7 +29,7 @@ import { DomSanitizer } from '@singletons';
templateUrl: 'core-iframe.html',
styleUrls: ['iframe.scss'],
})
export class CoreIframeComponent implements OnChanges {
export class CoreIframeComponent implements OnChanges, OnDestroy {
static loadingTimeout = 15000;
@ -39,17 +38,19 @@ export class CoreIframeComponent implements OnChanges {
@Input() iframeWidth?: string;
@Input() iframeHeight?: string;
@Input() allowFullscreen?: boolean | string;
@Input() showFullscreenOnToolbar?: boolean | string;
@Output() loaded: EventEmitter<HTMLIFrameElement> = new EventEmitter<HTMLIFrameElement>();
loading?: boolean;
safeUrl?: SafeResourceUrl;
displayHelp = false;
fullscreen = false;
protected logger: CoreLogger;
protected initialized = false;
initialized = false;
protected style?: HTMLStyleElement;
constructor() {
this.logger = CoreLogger.getInstance('CoreIframe');
this.loaded = new EventEmitter<HTMLIFrameElement>();
}
@ -71,6 +72,16 @@ export class CoreIframeComponent implements OnChanges {
this.iframeWidth = (this.iframeWidth && CoreDomUtils.formatPixelsSize(this.iframeWidth)) || '100%';
this.iframeHeight = (this.iframeHeight && CoreDomUtils.formatPixelsSize(this.iframeHeight)) || '100%';
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.
this.loading = !this.src || !CoreUrlUtils.isLocalFileUrl(this.src);
@ -120,4 +131,32 @@ export class CoreIframeComponent implements OnChanges {
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",
"digitalminor": "Digital minor",
"digitalminor_desc": "Please ask your parent/guardian to contact:",
"disablefullscreen": "Disable fullscreen",
"discard": "Discard",
"dismiss": "Dismiss",
"displayoptions": "Display options",
@ -122,6 +123,7 @@
"forcepasswordchangenotice": "You must change your password to proceed.",
"fulllistofcourses": "All courses",
"fullnameandsitename": "{{fullname}} ({{sitename}})",
"fullscreen": "Fullscreen",
"group": "Group",
"groupsseparate": "Separate 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.
.core-modal-fullscreen .modal-wrapper {
position: absolute;

View File

@ -236,6 +236,11 @@
--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-color: var(--black);
--core-combobox-border-color: var(--primary);