MOBILE-4109 book: Disable unactive pages

main
Noel De Martin 2022-11-17 11:37:00 +01:00
parent 56c4877b2e
commit 5370217058
7 changed files with 256 additions and 11 deletions

View File

@ -31,10 +31,10 @@
</ion-card>
<core-swipe-slides [manager]="manager" [options]="slidesOpts">
<ng-template let-chapter="item">
<ng-template let-chapter="item" let-active="active">
<div class="ion-padding">
<core-format-text [component]="component" [componentId]="cmId" [text]="chapter.content" contextLevel="module"
[contextInstanceId]="cmId" [courseId]="courseId"></core-format-text>
[contextInstanceId]="cmId" [courseId]="courseId" [disabled]="!active"></core-format-text>
<div class="ion-margin-top" *ngIf="chapter.tags?.length > 0">
<strong>{{ 'core.tag.tags' | translate }}: </strong>
<core-tag-list [tags]="chapter.tags"></core-tag-list>

View File

@ -0,0 +1,62 @@
// (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.
/**
* Wrapper class to control the interactivity of an element.
*/
export abstract class ElementController {
protected enabled: boolean;
constructor(enabled: boolean) {
this.enabled = enabled;
}
/**
* Enable element.
*/
enable(): void {
if (this.enabled) {
return;
}
this.enabled = true;
this.onEnabled();
}
/**
* Disable element.
*/
disable(): void {
if (!this.enabled) {
return;
}
this.enabled = false;
this.onDisabled();
}
/**
* Update underlying element to enable interactivity.
*/
abstract onEnabled(): void;
/**
* Update underlying element to disable interactivity.
*/
abstract onDisabled(): void;
}

View File

@ -0,0 +1,50 @@
// (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 { ElementController } from './ElementController';
export type FrameElement = HTMLIFrameElement | HTMLFrameElement | HTMLObjectElement | HTMLEmbedElement;
/**
* Wrapper class to control the interactivity of a frame element.
*/
export class FrameElementController extends ElementController {
private frame: FrameElement;
private placeholder: Node;
constructor(element: FrameElement, enabled: boolean) {
super(enabled);
this.frame = element;
this.placeholder = document.createComment('disabled frame placeholder');
enabled || this.onDisabled();
}
/**
* @inheritdoc
*/
onEnabled(): void {
this.placeholder.parentElement?.replaceChild(this.frame, this.placeholder);
}
/**
* @inheritdoc
*/
onDisabled(): void {
this.frame.parentElement?.replaceChild(this.placeholder, this.frame);
}
}

View File

@ -0,0 +1,81 @@
// (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 { CoreUtils } from '@services/utils/utils';
import { ElementController } from './ElementController';
/**
* Wrapper class to control the interactivity of a media element.
*/
export class MediaElementController extends ElementController {
private media: HTMLMediaElement;
private autoplay: boolean;
private playing?: boolean;
private playListener?: () => void;
private pauseListener?: () => void;
constructor(media: HTMLMediaElement, enabled: boolean) {
super(enabled);
this.media = media;
this.autoplay = media.autoplay;
media.autoplay = false;
enabled && this.onEnabled();
}
/**
* @inheritdoc
*/
onEnabled(): void {
const ready = this.playing ?? this.autoplay
? this.media.play()
: Promise.resolve();
ready
.then(() => this.addPlaybackEventListeners())
.catch(error => CoreUtils.logUnhandledError('Error enabling media element', error));
}
/**
* @inheritdoc
*/
async onDisabled(): Promise<void> {
this.removePlaybackEventListeners();
this.media.pause();
}
/**
* Start listening playback events.
*/
private addPlaybackEventListeners(): void {
this.media.addEventListener('play', this.playListener = () => this.playing = true);
this.media.addEventListener('pause', this.pauseListener = () => this.playing = false);
}
/**
* Stop listening playback events.
*/
private removePlaybackEventListeners(): void {
this.playListener && this.media.removeEventListener('play', this.playListener);
this.pauseListener && this.media.removeEventListener('pause', this.pauseListener);
delete this.playListener;
delete this.pauseListener;
}
}

View File

@ -1,5 +1,6 @@
<ion-slides *ngIf="loaded" (ionSlideWillChange)="slideWillChange()" (ionSlideDidChange)="slideDidChange()" [options]="options">
<ion-slide *ngFor="let item of items">
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item}"></ng-container>
<ion-slide *ngFor="let item of items; index as index">
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item, active: isActive(index)}">
</ng-container>
</ion-slide>
</ion-slides>

View File

@ -45,6 +45,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
protected unsubscribe?: () => void;
protected resizeListener: CoreEventObserver;
protected updateSlidesPromise?: Promise<void>;
protected activeSlideIndexes: number[] = [];
constructor(
elementRef: ElementRef<HTMLElement>,
@ -74,6 +75,16 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
return !!this.manager?.getSource().isLoaded();
}
/**
* Check whether the slide with the given index is active.
*
* @param index Slide index.
* @return Whether the slide is active.
*/
isActive(index: number): boolean {
return this.activeSlideIndexes.includes(index);
}
/**
* Initialize some properties based on the manager.
*/
@ -101,13 +112,15 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
}
// Validate that the initial index is inside the valid range.
const initialIndex = CoreMath.clamp(this.options.initialSlide as number, 0, items.length - 1);
const initialIndex = CoreMath.clamp(this.options.initialSlide, 0, items.length - 1);
const initialItemData = {
index: initialIndex,
item: items[initialIndex],
};
this.activeSlideIndexes = [initialIndex];
manager.setSelectedItem(items[initialIndex]);
this.onWillChange.emit(initialItemData);
this.onDidChange.emit(initialItemData);
@ -205,6 +218,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
return;
}
this.activeSlideIndexes.push(currentItemData.index);
this.manager?.setSelectedItem(currentItemData.item);
this.onWillChange.emit(currentItemData);
@ -219,9 +233,13 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
async slideDidChange(): Promise<void> {
const currentItemData = await this.getCurrentSlideItemData();
if (!currentItemData) {
this.activeSlideIndexes = [];
return;
}
this.activeSlideIndexes = [currentItemData.index];
this.onDidChange.emit(currentItemData);
await this.applyScrollOnChange();
@ -304,6 +322,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
* @todo Change unknown with the right type once Swiper library is used.
*/
export type CoreSwipeSlidesOptions = Record<string, unknown> & {
initialSlide?: number;
scrollOnChange?: 'top' | 'none'; // Scroll behaviour on change slide. By default, none.
};

View File

@ -51,6 +51,9 @@ import { CoreDom } from '@singletons/dom';
import { CoreEvents } from '@singletons/events';
import { CoreRefreshContext, CORE_REFRESH_CONTEXT } from '@/core/utils/refresh-context';
import { CorePlatform } from '@services/platform';
import { ElementController } from '@classes/element-controllers/ElementController';
import { MediaElementController } from '@classes/element-controllers/MediaElementController';
import { FrameElementController } from '@classes/element-controllers/FrameElementController';
/**
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
@ -84,6 +87,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
@Input() captureLinks?: boolean; // Whether links should tried to be opened inside the app. Defaults to true.
@Input() openLinksInApp?: boolean; // Whether links should be opened in InAppBrowser.
@Input() hideIfEmpty = false; // If true, the tag will contain nothing if text is empty.
@Input() disabled?: boolean; // If disabled, autoplay elements will be disabled.
@Input() fullOnClick?: boolean | string; // @deprecated on 4.0 Won't do anything.
@Input() fullTitle?: string; // @deprecated on 4.0 Won't do anything.
@ -96,6 +100,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
@Output() onClick: EventEmitter<void> = new EventEmitter(); // Called when clicked.
protected element: HTMLElement;
protected elementControllers: ElementController[] = [];
protected emptyText = '';
protected domPromises: CoreCancellablePromise<void>[] = [];
protected domElementPromise?: CoreCancellablePromise<void>;
@ -127,6 +132,14 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (changes.text || changes.filter || changes.contextLevel || changes.contextInstanceId) {
this.formatAndRenderContents();
return;
}
if ('disabled' in changes) {
const disabled = changes['disabled'].currentValue;
this.elementControllers.forEach(controller => disabled ? controller.disable() : controller.enable());
}
}
@ -352,6 +365,8 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
// Move the children to the current element to be able to calculate the height.
CoreDomUtils.moveChildren(result.div, this.element);
this.elementControllers = result.elementControllers;
await CoreUtils.nextTick();
// Use collapsible-item directive instead.
@ -432,13 +447,14 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
div.innerHTML = formatted;
this.treatHTMLElements(div, site);
const elementControllers = this.treatHTMLElements(div, site);
return {
div,
filters,
options,
siteId,
elementControllers,
};
}
@ -449,7 +465,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
* @param site Site instance.
* @return Promise resolved when done.
*/
protected treatHTMLElements(div: HTMLElement, site?: CoreSite): void {
protected treatHTMLElements(div: HTMLElement, site?: CoreSite): ElementController[] {
const images = Array.from(div.querySelectorAll('img'));
const anchors = Array.from(div.querySelectorAll('a'));
const audios = Array.from(div.querySelectorAll('audio'));
@ -498,16 +514,22 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
});
}
audios.forEach((audio) => {
const audioControllers = audios.map(audio => {
this.treatMedia(audio);
return new MediaElementController(audio, !this.disabled);
});
videos.forEach((video) => {
const videoControllers = videos.map(video => {
this.treatMedia(video, true);
return new MediaElementController(video, !this.disabled);
});
iframes.forEach((iframe) => {
const iframeControllers = iframes.map(iframe => {
promises.push(this.treatIframe(iframe, site));
return new FrameElementController(iframe, !this.disabled);
});
svgImages.forEach((image) => {
@ -539,8 +561,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
});
// Handle all kind of frames.
frames.forEach((frame: HTMLFrameElement | HTMLObjectElement | HTMLEmbedElement) => {
const frameControllers = frames.map((frame: HTMLFrameElement | HTMLObjectElement | HTMLEmbedElement) => {
CoreIframeUtils.treatFrame(frame, false);
return new FrameElementController(frame, !this.disabled);
});
CoreDomUtils.handleBootstrapTooltips(div);
@ -562,6 +586,13 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
// Run asynchronous operations in the background to avoid blocking rendering.
Promise.all(promises).catch(error => CoreUtils.logUnhandledError('Error treating format-text elements', error));
return [
...videoControllers,
...audioControllers,
...iframeControllers,
...frameControllers,
];
}
/**
@ -903,6 +934,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
type FormatContentsResult = {
div: HTMLElement;
filters: CoreFilterFilter[];
elementControllers: ElementController[];
options: CoreFilterFormatTextOptions;
siteId?: string;
};