MOBILE-2235 h5p: Show placeholder instead of H5P directly

main
Dani Palou 2019-10-29 16:07:22 +01:00
parent 5e2e1d1a24
commit 690544afbf
19 changed files with 341 additions and 22 deletions

View File

@ -0,0 +1,34 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { IonicModule } from 'ionic-angular';
import { CoreFilterDelegate } from '@core/filter/providers/delegate';
import { AddonFilterDisplayH5PHandler } from './providers/handler';
@NgModule({
declarations: [
],
imports: [
IonicModule
],
providers: [
AddonFilterDisplayH5PHandler
]
})
export class AddonFilterDisplayH5PModule {
constructor(filterDelegate: CoreFilterDelegate, handler: AddonFilterDisplayH5PHandler) {
filterDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,93 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { CoreFilterDefaultHandler } from '@core/filter/providers/default-filter';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@core/filter/providers/filter';
import { CoreH5PPlayerComponent } from '@core/h5p/components/h5p-player/h5p-player';
/**
* Handler to support the Display H5P filter.
*/
@Injectable()
export class AddonFilterDisplayH5PHandler extends CoreFilterDefaultHandler {
name = 'AddonFilterDisplayH5PHandler';
filterName = 'displayh5p';
protected template = document.createElement('template'); // A template element to convert HTML to element.
constructor(protected factoryResolver: ComponentFactoryResolver) {
super();
}
/**
* Filter some text.
*
* @param text The text to filter.
* @param filter The filter.
* @param options Options passed to the filters.
* @param siteId Site ID. If not defined, current site.
* @return Filtered text (or promise resolved with the filtered text).
*/
filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
: string | Promise<string> {
this.template.innerHTML = text;
const h5pIframes = <HTMLIFrameElement[]> Array.from(this.template.content.querySelectorAll('iframe.h5p-iframe'));
// Replace all iframes with an empty div that will be treated in handleHtml.
h5pIframes.forEach((iframe) => {
const placeholder = document.createElement('div');
placeholder.classList.add('core-h5p-tmp-placeholder');
placeholder.setAttribute('data-player-src', iframe.src);
iframe.parentElement.replaceChild(placeholder, iframe);
});
return this.template.innerHTML;
}
/**
* Handle HTML. This function is called after "filter", and it will receive an HTMLElement containing the text that was
* filtered.
*
* @param container The HTML container to handle.
* @param filter The filter.
* @param options Options passed to the filters.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param siteId Site ID. If not defined, current site.
* @return If async, promise resolved when done.
*/
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions,
viewContainerRef: ViewContainerRef, siteId?: string): void | Promise<void> {
const placeholders = <HTMLElement[]> Array.from(container.querySelectorAll('div.core-h5p-tmp-placeholder'));
placeholders.forEach((placeholder) => {
const url = placeholder.getAttribute('data-player-src');
// Create the component to display the player.
const factory = this.factoryResolver.resolveComponentFactory(CoreH5PPlayerComponent),
componentRef = viewContainerRef.createComponent(factory);
componentRef.instance.src = url;
// Move the component to its right position.
placeholder.parentElement.replaceChild(componentRef.instance.elementRef.nativeElement, placeholder);
});
}
}

View File

@ -17,6 +17,7 @@ import { AddonFilterActivityNamesModule } from './activitynames/activitynames.mo
import { AddonFilterAlgebraModule } from './algebra/algebra.module';
import { AddonFilterCensorModule } from './censor/censor.module';
import { AddonFilterDataModule } from './data/data.module';
import { AddonFilterDisplayH5PModule } from './displayh5p/displayh5p.module';
import { AddonFilterEmailProtectModule } from './emailprotect/emailprotect.module';
import { AddonFilterEmoticonModule } from './emoticon/emoticon.module';
import { AddonFilterGlossaryModule } from './glossary/glossary.module';
@ -34,6 +35,7 @@ import { AddonFilterUrlToLinkModule } from './urltolink/urltolink.module';
AddonFilterAlgebraModule,
AddonFilterCensorModule,
AddonFilterDataModule,
AddonFilterDisplayH5PModule,
AddonFilterEmailProtectModule,
AddonFilterEmoticonModule,
AddonFilterGlossaryModule,

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, ViewContainerRef } from '@angular/core';
import { CoreFilterDefaultHandler } from '@core/filter/providers/default-filter';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@core/filter/providers/filter';
import { CoreEventsProvider } from '@providers/events';
@ -161,11 +161,12 @@ export class AddonFilterMathJaxLoaderHandler extends CoreFilterDefaultHandler {
* @param container The HTML container to handle.
* @param filter The filter.
* @param options Options passed to the filters.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param siteId Site ID. If not defined, current site.
* @return If async, promise resolved when done.
*/
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
: void | Promise<void> {
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions,
viewContainerRef: ViewContainerRef, siteId?: string): void | Promise<void> {
return this.waitForReady().then(() => {
this.window.M.filter_mathjaxloader.typeset(container);

View File

@ -27,6 +27,8 @@ export class AddonFilterMediaPluginHandler extends CoreFilterDefaultHandler {
name = 'AddonFilterMediaPluginHandler';
filterName = 'mediaplugin';
protected template = document.createElement('template'); // A template element to convert HTML to element.
constructor(private textUtils: CoreTextUtilsProvider,
private urlUtils: CoreUrlUtilsProvider) {
super();
@ -44,16 +46,15 @@ export class AddonFilterMediaPluginHandler extends CoreFilterDefaultHandler {
filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
: string | Promise<string> {
const div = document.createElement('div');
div.innerHTML = text;
this.template.innerHTML = text;
const videos = Array.from(div.querySelectorAll('video'));
const videos = Array.from(this.template.content.querySelectorAll('video'));
videos.forEach((video) => {
this.treatVideoFilters(video);
});
return div.innerHTML;
return this.template.innerHTML;
}
/**

View File

@ -83,6 +83,7 @@ import { CoreBlockModule } from '@core/block/block.module';
import { CoreRatingModule } from '@core/rating/rating.module';
import { CoreTagModule } from '@core/tag/tag.module';
import { CoreFilterModule } from '@core/filter/filter.module';
import { CoreH5PModule } from '@core/h5p/h5p.module';
// Addon modules.
import { AddonBadgesModule } from '@addon/badges/badges.module';
@ -230,6 +231,7 @@ export const WP_PROVIDER: any = null;
CorePushNotificationsModule,
CoreTagModule,
CoreFilterModule,
CoreH5PModule,
AddonBadgesModule,
AddonBlogModule,
AddonCalendarModule,

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 345 150" style="enable-background:new 0 0 345 150;" xml:space="preserve">
<g>
<path d="M325.7,14.7C317.6,6.9,305.3,3,289,3h-43.5H234v31h-66l-5.4,22.2c4.5-2.1,10.9-4.2,15.3-5.3c4.4-1.1,8.8-0.9,13.1-0.9
c14.6,0,26.5,4.5,35.6,13.3c9.1,8.8,13.6,20,13.6,33.4c0,9.4-2.3,18.5-7,27.2s-11.3,15.4-19.9,20c-3.1,1.6-6.5,3.1-10.2,4.1h42.4
H259V95h25c18.2,0,31.7-4.2,40.6-12.5s13.3-19.9,13.3-34.6C337.9,33.6,333.8,22.5,325.7,14.7z M288.7,60.6c-3.5,3-9.6,4.4-18.3,4.4
H259V33h13.2c8.4,0,14.2,1.5,17.2,4.7c3.1,3.2,4.6,6.9,4.6,11.5C294,53.9,292.2,57.6,288.7,60.6z"/>
<path d="M176.5,76.3c-7.9,0-14.7,4.6-18,11.2L119,81.9L136.8,3h-23.6H101v62H51V3H7v145h44V95h50v53h12.2h42
c-6.7-2-12.5-4.6-17.2-8.1c-4.8-3.6-8.7-7.7-11.7-12.3c-3-4.6-5.3-9.7-7.3-16.5l39.6-5.7c3.3,6.6,10.1,11.1,17.9,11.1
c11.1,0,20.1-9,20.1-20.1S187.5,76.3,176.5,76.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1550,6 +1550,7 @@
"core.group": "Group",
"core.groupsseparate": "Separate groups",
"core.groupsvisible": "Visible groups",
"core.h5p.play": "Play H5P",
"core.hasdatatosync": "This {{$a}} has offline data to be synchronised.",
"core.help": "Help",
"core.hide": "Hide",

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, ViewContainerRef } from '@angular/core';
import { CoreFilterHandler } from './delegate';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from './filter';
import { CoreSite } from '@classes/site';
@ -50,11 +50,12 @@ export class CoreFilterDefaultHandler implements CoreFilterHandler {
* @param container The HTML container to handle.
* @param filter The filter.
* @param options Options passed to the filters.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param siteId Site ID. If not defined, current site.
* @return If async, promise resolved when done.
*/
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
: void | Promise<void> {
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions,
viewContainerRef: ViewContainerRef, siteId?: string): void | Promise<void> {
// To be overridden.
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, ViewContainerRef } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
@ -48,11 +48,12 @@ export interface CoreFilterHandler extends CoreDelegateHandler {
* @param container The HTML container to handle.
* @param filter The filter.
* @param options Options passed to the filters.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param siteId Site ID. If not defined, current site.
* @return If async, promise resolved when done.
*/
handleHtml?(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
: void | Promise<void>;
handleHtml?(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions,
viewContainerRef: ViewContainerRef, siteId?: string): void | Promise<void>;
/**
* Check if the filter should be applied in a certain site based on some filter options.
@ -156,13 +157,14 @@ export class CoreFilterDelegate extends CoreDelegate {
*
* @param container The HTML container to handle.
* @param filters Filters to apply.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param options Options passed to the filters.
* @param skipFilters Names of filters that shouldn't be applied.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
handleHtml(container: HTMLElement, filters: CoreFilterFilter[], options?: any, skipFilters?: string[], siteId?: string)
: Promise<any> {
handleHtml(container: HTMLElement, filters: CoreFilterFilter[], viewContainerRef?: ViewContainerRef, options?: any,
skipFilters?: string[], siteId?: string): Promise<any> {
// Wait for filters to be initialized.
return this.handlersInitPromise.then(() => {
@ -182,7 +184,7 @@ export class CoreFilterDelegate extends CoreDelegate {
promise = promise.then(() => {
return Promise.resolve(this.executeFunctionOnEnabled(filter.filter, 'handleHtml',
[container, filter, options, siteId])).catch((error) => {
[container, filter, options, viewContainerRef, siteId])).catch((error) => {
this.logger.error('Error handling HTML' + filter.filter, error);
});
});

View File

@ -0,0 +1,43 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreH5PPlayerComponent } from './h5p-player/h5p-player';
import { CoreComponentsModule } from '@components/components.module';
@NgModule({
declarations: [
CoreH5PPlayerComponent
],
imports: [
CommonModule,
IonicModule,
CoreDirectivesModule,
TranslateModule.forChild(),
CoreComponentsModule
],
providers: [
],
exports: [
CoreH5PPlayerComponent
],
entryComponents: [
CoreH5PPlayerComponent
]
})
export class CoreH5PComponentsModule {}

View File

@ -0,0 +1,9 @@
<div *ngIf="!showPackage" class="core-h5p-placeholder">
<button *ngIf="!loading" class="core-h5p-placeholder-play-button" ion-button icon-only clear color="dark" (click)="play($event)">
<core-icon name="fa-play-circle"></core-icon>
</button>
<ion-spinner *ngIf="loading" class="core-h5p-placeholder-spinner"></ion-spinner>
</div>
<core-iframe *ngIf="showPackage" [src]="src"></core-iframe>

View File

@ -0,0 +1,24 @@
ion-app.app-root core-h5p-player {
.core-h5p-placeholder {
position: relative;
width: 100%;
height: 230px;
background: url('../assets/img/icons/h5p.svg') center top 25px / 100px auto no-repeat $core-h5p-placeholder-bg-color;
.core-h5p-placeholder-play-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30px;
min-height: 50px;
}
.core-h5p-placeholder-download-container {
position: absolute;
right: 10px;
font-size: 1.8em;
}
}
}

View File

@ -0,0 +1,56 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, Input, ElementRef } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
/**
* Component to render an H5P package.
*/
@Component({
selector: 'core-h5p-player',
templateUrl: 'core-h5p-player.html'
})
export class CoreH5PPlayerComponent {
@Input() src: string; // The URL of the player to display the H5P package.
showPackage = false;
loading = false;
status: string;
canDownload: boolean;
calculating = true;
constructor(public elementRef: ElementRef,
protected sitesProvider: CoreSitesProvider) {
}
/**
* Play the H5P.
*
* @param e Event.
*/
play(e: MouseEvent): void {
e.preventDefault();
e.stopPropagation();
this.loading = true;
// Get auto-login URL so the user is automatically authenticated.
this.sitesProvider.getCurrentSite().getAutoLoginUrl(this.src, false).then((url) => {
this.src = url;
this.loading = false;
this.showPackage = true;
});
}
}

View File

@ -0,0 +1,27 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreH5PComponentsModule } from './components/components.module';
@NgModule({
declarations: [],
imports: [
CoreH5PComponentsModule
],
providers: [
],
exports: []
})
export class CoreH5PModule { }

View File

@ -0,0 +1,3 @@
{
"play": "Play H5P"
}

View File

@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core';
import {
Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional, ViewContainerRef
} from '@angular/core';
import { Platform, NavController, Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
@ -90,7 +92,8 @@ export class CoreFormatTextDirective implements OnChanges {
private eventsProvider: CoreEventsProvider,
private filterProvider: CoreFilterProvider,
private filterHelper: CoreFilterHelperProvider,
private filterDelegate: CoreFilterDelegate) {
private filterDelegate: CoreFilterDelegate,
private viewContainerRef: ViewContainerRef) {
this.element = element.nativeElement;
this.element.classList.add('opacity-hide'); // Hide contents until they're treated.
@ -371,7 +374,8 @@ export class CoreFormatTextDirective implements OnChanges {
if (result.options.filter) {
// Let filters hnadle HTML. We do it here because we don't want them to block the render of the text.
this.filterDelegate.handleHtml(this.element, result.filters, result.options, [], result.siteId);
this.filterDelegate.handleHtml(this.element, result.filters, this.viewContainerRef, result.options, [],
result.siteId);
}
this.element.classList.remove('core-disable-media-adapt');

View File

@ -514,10 +514,9 @@ export class CoreTextUtilsProvider {
return true;
}
const div = document.createElement('div');
div.innerHTML = content;
this.template.innerHTML = content;
return div.textContent === '' && div.querySelector('img, object, hr') === null;
return this.template.textContent === '' && this.template.content.querySelector('img, object, hr') === null;
}
/**

View File

@ -369,6 +369,9 @@ $core-question-state-incorrect-color: $red-light !default;
$core-dd-question-selected-shadow: 2px 2px 4px $gray-dark !default;
$core-dd-question-colors: $white, $blue-light, #DCDCDC, #D8BFD8, #87CEFA, #DAA520, #FFD700, #F0E68C !default;
// H5P variables.
$core-h5p-placeholder-bg-color: $gray-dark !default;
// Mixins
// -------------------------
@mixin core-transition($where: all, $time: 500ms) {