From 5370217058baa9132bbc8db69335f00d485b783f Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 17 Nov 2022 11:37:00 +0100 Subject: [PATCH] MOBILE-4109 book: Disable unactive pages --- .../mod/book/pages/contents/contents.html | 4 +- .../element-controllers/ElementController.ts | 62 ++++++++++++++ .../FrameElementController.ts | 50 ++++++++++++ .../MediaElementController.ts | 81 +++++++++++++++++++ .../components/swipe-slides/swipe-slides.html | 5 +- .../components/swipe-slides/swipe-slides.ts | 21 ++++- src/core/directives/format-text.ts | 44 ++++++++-- 7 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 src/core/classes/element-controllers/ElementController.ts create mode 100644 src/core/classes/element-controllers/FrameElementController.ts create mode 100644 src/core/classes/element-controllers/MediaElementController.ts diff --git a/src/addons/mod/book/pages/contents/contents.html b/src/addons/mod/book/pages/contents/contents.html index 0f7605f0c..a34486e0a 100644 --- a/src/addons/mod/book/pages/contents/contents.html +++ b/src/addons/mod/book/pages/contents/contents.html @@ -31,10 +31,10 @@ - +
+ [contextInstanceId]="cmId" [courseId]="courseId" [disabled]="!active">
{{ 'core.tag.tags' | translate }}: diff --git a/src/core/classes/element-controllers/ElementController.ts b/src/core/classes/element-controllers/ElementController.ts new file mode 100644 index 000000000..e4ef1b000 --- /dev/null +++ b/src/core/classes/element-controllers/ElementController.ts @@ -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; + +} diff --git a/src/core/classes/element-controllers/FrameElementController.ts b/src/core/classes/element-controllers/FrameElementController.ts new file mode 100644 index 000000000..e449de41b --- /dev/null +++ b/src/core/classes/element-controllers/FrameElementController.ts @@ -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); + } + +} diff --git a/src/core/classes/element-controllers/MediaElementController.ts b/src/core/classes/element-controllers/MediaElementController.ts new file mode 100644 index 000000000..0ae911fc0 --- /dev/null +++ b/src/core/classes/element-controllers/MediaElementController.ts @@ -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 { + 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; + } + +} diff --git a/src/core/components/swipe-slides/swipe-slides.html b/src/core/components/swipe-slides/swipe-slides.html index fc18d8c18..8b0860750 100644 --- a/src/core/components/swipe-slides/swipe-slides.html +++ b/src/core/components/swipe-slides/swipe-slides.html @@ -1,5 +1,6 @@ - - + + + diff --git a/src/core/components/swipe-slides/swipe-slides.ts b/src/core/components/swipe-slides/swipe-slides.ts index bb25cfe22..561b55356 100644 --- a/src/core/components/swipe-slides/swipe-slides.ts +++ b/src/core/components/swipe-slides/swipe-slides.ts @@ -45,6 +45,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe protected unsubscribe?: () => void; protected resizeListener: CoreEventObserver; protected updateSlidesPromise?: Promise; + protected activeSlideIndexes: number[] = []; constructor( elementRef: ElementRef, @@ -74,6 +75,16 @@ export class CoreSwipeSlidesComponent 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 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 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 implements OnChanges, OnDe async slideDidChange(): Promise { 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 implements OnChanges, OnDe * @todo Change unknown with the right type once Swiper library is used. */ export type CoreSwipeSlidesOptions = Record & { + initialSlide?: number; scrollOnChange?: 'top' | 'none'; // Scroll behaviour on change slide. By default, none. }; diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 090603386..39f35765b 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -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 = new EventEmitter(); // Called when clicked. protected element: HTMLElement; + protected elementControllers: ElementController[] = []; protected emptyText = ''; protected domPromises: CoreCancellablePromise[] = []; protected domElementPromise?: CoreCancellablePromise; @@ -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; };