Skip to content

Commit 21bb6f6

Browse files
authored
Upgrade @fancyapps/ui to v6 (#6362)
* Upgrade `@fancyapps/ui` to v6 * Add localization phrases for Fancybox * Refactor language items to use camelCase naming convention * Only display the Fancybox if the `data-type` has been set to `image` or `video`. * Display consent box for external media sources * Extend the automatic display of Fancybox to include YouTube and Vimeo video links.
1 parent 2f3517a commit 21bb6f6

28 files changed

Lines changed: 452 additions & 118 deletions

File tree

com.woltlab.wcf/templates/attachments.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
{foreach from=$attachmentList->getGroupedObjects($objectID) item=attachment}
99
{if $attachment->showAsImage() && !$attachment->isEmbedded()}
1010
<li class="attachmentThumbnail" data-attachment-id="{$attachment->attachmentID}">
11-
<a href="{$attachment->getLink()}"{if $attachment->canDownload()} data-fancybox="message-{$attachmentList->getObjectTypeName()}-{$objectID}" data-caption="{$attachment->filename}" aria-title="{lang}wcf.attachment.image.title{/lang}"{/if}>
11+
<a href="{$attachment->getLink()}"{if $attachment->canDownload()} data-type="image" data-fancybox="message-{$attachmentList->getObjectTypeName()}-{$objectID}" data-caption="{$attachment->filename}" aria-title="{lang}wcf.attachment.image.title{/lang}"{/if}>
1212
<div class="attachmentThumbnailContainer">
1313
<span class="attachmentThumbnailImage">
1414
{if $attachment->hasThumbnail()}

com.woltlab.wcf/templates/entryAttachments.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
{foreach from=$attachmentList->getGroupedObjects($objectID) item=attachment}
99
{if $attachment->showAsImage() && !$attachment->isEmbedded()}
1010
<li class="attachmentThumbnail" data-attachment-id="{$attachment->attachmentID}">
11-
<a href="{$attachment->getLink()}"{if $attachment->canDownload()} data-fancybox="message-{$attachmentList->getObjectTypeName()}-{$objectID}" data-caption="{$attachment->filename}" aria-title="{lang}wcf.attachment.image.title{/lang}"{/if}>
11+
<a href="{$attachment->getLink()}"{if $attachment->canDownload()} data-type="image" data-fancybox="message-{$attachmentList->getObjectTypeName()}-{$objectID}" data-caption="{$attachment->filename}" aria-title="{lang}wcf.attachment.image.title{/lang}"{/if}>
1212
<div class="attachmentThumbnailContainer">
1313
<span class="attachmentThumbnailImage">
1414
{if $attachment->hasThumbnail()}

com.woltlab.wcf/templates/shared_bbcode_wsm.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{if $media->isImage}
55
{if $thumbnailSize != 'original'}
66
{if !$removeLinks}
7-
<a href="{$mediaLink}" data-caption="{$media->title}" class="embeddedAttachmentLink" data-fancybox="message-{$activeMessageObjectType}-{$activeMessageID}">
7+
<a href="{$mediaLink}" data-caption="{$media->title}" class="embeddedAttachmentLink" data-type="image" data-fancybox="message-{$activeMessageObjectType}-{$activeMessageID}">
88
{/if}
99
<img src="{$thumbnailLink}" alt="{$media->altText}" title="{$media->title}" width="{$media->getThumbnailWidth($thumbnailSize)}" height="{$media->getThumbnailHeight($thumbnailSize)}" loading="lazy">
1010
{if !$removeLinks}

com.woltlab.wcf/templates/shared_imageViewer.tpl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
11
{if !$__imageViewerLoaded|isset}
22
<script data-eager="true">
3+
{jsphrase name='wcf.fancybox.imageError'}
4+
{jsphrase name='wcf.fancybox.moveUp'}
5+
{jsphrase name='wcf.fancybox.moveDown'}
6+
{jsphrase name='wcf.fancybox.moveLeft'}
7+
{jsphrase name='wcf.fancybox.moveRight'}
8+
{jsphrase name='wcf.fancybox.zoomIn'}
9+
{jsphrase name='wcf.fancybox.zoomOut'}
10+
{jsphrase name='wcf.fancybox.toggleFull'}
11+
{jsphrase name='wcf.fancybox.toggle1to1'}
12+
{jsphrase name='wcf.fancybox.iterateZoom'}
13+
{jsphrase name='wcf.fancybox.rotateCcw'}
14+
{jsphrase name='wcf.fancybox.rotateCw'}
15+
{jsphrase name='wcf.fancybox.flipX'}
16+
{jsphrase name='wcf.fancybox.flipY'}
17+
{jsphrase name='wcf.fancybox.reset'}
18+
{jsphrase name='wcf.fancybox.error'}
19+
{jsphrase name='wcf.fancybox.next'}
20+
{jsphrase name='wcf.fancybox.prev'}
21+
{jsphrase name='wcf.fancybox.goto'}
22+
{jsphrase name='wcf.fancybox.download'}
23+
{jsphrase name='wcf.fancybox.toggleFullscreen'}
24+
{jsphrase name='wcf.fancybox.toggleExpand'}
25+
{jsphrase name='wcf.fancybox.toggleThumbs'}
26+
{jsphrase name='wcf.fancybox.toggleAutoplay'}
27+
{jsphrase name='wcf.fancybox.close'}
28+
{jsphrase name='wcf.fancybox.next'}
29+
{jsphrase name='wcf.fancybox.prev'}
30+
{jsphrase name='wcf.fancybox.modal'}
31+
{jsphrase name='wcf.fancybox.elementNotFound'}
32+
{jsphrase name='wcf.fancybox.iframeError'}
33+
334
{
435
let stylesheet = document.getElementById("fancybox-stylesheet");
536
if (stylesheet === null) {
@@ -13,6 +44,11 @@
1344
}
1445
}
1546
</script>
47+
{if MESSAGE_ENABLE_USER_CONSENT}
48+
<template id="consentImageViewer" data-show-all-media="{if $__wcf->getUser()->userID && $__wcf->getUser()->getUserOption('enableEmbeddedMedia')}1{else}0{/if}">
49+
{include file="messageUserConsent" target='' host='' url='' sandbox=true}
50+
</template>
51+
{/if}
1652

1753
{assign var=__imageViewerLoaded value=true}
1854
{/if}

com.woltlab.wcf/templates/shared_uploadFieldComponent.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
{foreach from=$uploadFieldFiles item=file}
1414
<li class="box64 uploadedFile" data-unique-file-id="{$file->getUniqueFileId()}">
1515
{if $file->isImage()}
16-
<a href="{$file->getImage()}" data-fancybox data-caption="{$file->getFilename()}">
16+
<a href="{$file->getImage()}" data-type="image" data-fancybox data-caption="{$file->getFilename()}">
1717
<img src="{$file->getImage()}" width="{$file->getWidth()}" height="{$file->getHeight()}" loading="lazy" alt="" class="formUploadHandlerContentListImage">
1818
</a>
1919
{else}

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@ckeditor/ckeditor5-inspector": "^4.1.0",
17-
"@fancyapps/ui": "^5.0.36",
17+
"@fancyapps/ui": "^6.0.5",
1818
"@googlemaps/markerclusterer": "2.5.3",
1919
"@types/facebook-js-sdk": "^3.3.12",
2020
"@types/google.maps": "^3.58.1",
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* @author Olaf Braun
3+
* @copyright 2001-2025 WoltLab GmbH
4+
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
5+
*/
6+
7+
import { CarouselSlide, FancyboxInstance } from "@fancyapps/ui";
8+
import { CarouselInstance } from "@fancyapps/ui/dist/carousel/carousel";
9+
import { allExternalMediaEnabled } from "WoltLabSuite/Core/Ui/Message/UserConsent";
10+
import { stringToBool } from "WoltLabSuite/Core/Core";
11+
12+
export class ConsentPlugin {
13+
#api: FancyboxInstance | undefined = undefined;
14+
#sliderToType = new Map<CarouselSlide, string | undefined>();
15+
#templateNode: HTMLTemplateElement | undefined;
16+
#onAddSlideEvent = this.#onAddSlide.bind(this);
17+
#onAttachSlideElEvent = this.#onAttachSlideEl.bind(this);
18+
19+
constructor() {
20+
this.#templateNode = document.getElementById("consentImageViewer") as HTMLTemplateElement;
21+
}
22+
23+
init(api: FancyboxInstance): void {
24+
if (this.#showAllMedia()) {
25+
return;
26+
}
27+
28+
this.#api = api;
29+
30+
api.on("Carousel.addSlide", this.#onAddSlideEvent).on("Carousel.attachSlideEl", this.#onAttachSlideElEvent);
31+
}
32+
33+
destroy(): void {
34+
this.#api
35+
?.off("Carousel.addSlide", this.#onAddSlideEvent)
36+
?.off("Carousel.attachSlideEl", this.#onAttachSlideElEvent);
37+
}
38+
39+
#onAddSlide(_: FancyboxInstance, __: CarouselInstance, slide: CarouselSlide): void {
40+
if (!slide.src) {
41+
return;
42+
}
43+
44+
if (this.#isExternalURL(slide.src)) {
45+
this.#sliderToType.set(slide, slide.type);
46+
slide.type = "consent";
47+
}
48+
}
49+
50+
#onAttachSlideEl(_: FancyboxInstance, carousel: CarouselInstance, slide: CarouselSlide): void {
51+
if (slide.type !== "consent") {
52+
return;
53+
}
54+
55+
slide.el!.innerHTML = "";
56+
slide.el!.append(this.getConsentHTML(carousel, slide));
57+
}
58+
59+
private getConsentHTML(carousel: CarouselInstance, slide: CarouselSlide): DocumentFragment {
60+
const clone = this.#templateNode!.content.cloneNode(true) as DocumentFragment;
61+
62+
const messageUserConsent = clone.querySelector(".messageUserConsent") as HTMLElement;
63+
messageUserConsent.dataset.payload = "";
64+
65+
const externalURL = clone.querySelector(".externalURL") as HTMLAnchorElement;
66+
const url = new URL(slide.src!);
67+
externalURL.href = slide.src!;
68+
externalURL.innerText = url.host;
69+
70+
const button = clone.querySelector(".jsButtonMessageUserConsentEnable") as HTMLButtonElement;
71+
button.addEventListener("click", () => {
72+
this.destroy();
73+
74+
this.#templateNode!.dataset.showAllMedia = "1";
75+
76+
carousel
77+
.getSlides()
78+
.filter((slide) => slide.type === "consent")
79+
.forEach((slide) => {
80+
slide.type = this.#sliderToType.get(slide);
81+
slide.el!.innerHTML = "";
82+
});
83+
84+
// refresh current slide content
85+
carousel.emit("detachSlideEl", slide);
86+
carousel.emit("attachSlideEl", slide);
87+
});
88+
89+
return clone;
90+
}
91+
92+
#showAllMedia(): boolean {
93+
if (!this.#templateNode) {
94+
return true;
95+
}
96+
97+
if (stringToBool(this.#templateNode.dataset.showAllMedia!)) {
98+
return true;
99+
}
100+
101+
return allExternalMediaEnabled();
102+
}
103+
104+
#isExternalURL(url: string): boolean {
105+
return new URL(url).origin !== window.location.origin;
106+
}
107+
}
Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,78 @@
1-
import { Fancybox } from "@fancyapps/ui";
2-
import { userSlideType } from "@fancyapps/ui/types/Carousel/types";
3-
import { OptionsType } from "@fancyapps/ui/types/Fancybox/options";
1+
/**
2+
* @author Olaf Braun
3+
* @copyright 2001-2025 WoltLab GmbH
4+
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
5+
*/
6+
7+
import { Fancybox, CarouselSlide, FancyboxInstance } from "@fancyapps/ui";
48
import { getPageOverlayContainer } from "WoltLabSuite/Core/Helper/PageOverlay";
9+
import { getPhrase } from "WoltLabSuite/Core/Language";
10+
import { ConsentPlugin } from "./Fancybox/ConsentPlugin";
511

6-
const LOCALES = ["cs", "de", "en", "es", "fr", "it", "lv", "pl", "sk"];
12+
setDefaultConfig();
713

814
export function setup() {
9-
void getDefaultConfig().then((config) => {
10-
Fancybox.bind("[data-fancybox]", config);
11-
});
15+
Fancybox.bind(
16+
'[data-fancybox]:is([data-type="image"],[data-type="youtube"],[data-type="vimeo"],[data-type="video"])',
17+
);
1218
}
1319

1420
export function setupLegacy() {
15-
void getDefaultConfig().then((config) => {
16-
Fancybox.bind(".jsImageViewer", {
17-
...config,
18-
groupAll: true,
19-
});
21+
Fancybox.bind(".jsImageViewer", {
22+
groupAll: true,
2023
});
2124
}
2225

23-
export async function createFancybox(userSlides?: Array<userSlideType>): Promise<Fancybox> {
24-
return new Fancybox(userSlides, await getDefaultConfig());
26+
export function showFancybox(userSlides?: Array<CarouselSlide>): FancyboxInstance {
27+
return Fancybox.show(userSlides);
2528
}
2629

27-
async function getDefaultConfig(): Promise<Partial<OptionsType>> {
28-
return {
29-
l10n: await getLocalization(),
30-
parentEl: getPageOverlayContainer(),
31-
Html: {
32-
videoAutoplay: false,
30+
function setDefaultConfig(): void {
31+
const defaultConfig = Fancybox.getDefaults();
32+
defaultConfig.l10n = getLocalization();
33+
defaultConfig.parentEl = getPageOverlayContainer();
34+
defaultConfig.Carousel = {
35+
Video: {
36+
autoplay: false,
3337
},
3438
};
35-
}
36-
37-
export async function getLocalization(): Promise<Record<string, string>> {
38-
let locale = document.documentElement.lang;
39-
40-
if (!LOCALES.includes(locale)) {
41-
locale = "en";
39+
if (!defaultConfig.plugins) {
40+
defaultConfig.plugins = {};
4241
}
42+
defaultConfig.plugins.consent = () => {
43+
return new ConsentPlugin();
44+
};
45+
}
4346

44-
return (await import(`@fancyapps/ui/l10n/${locale}`))[locale];
47+
export function getLocalization(): Record<string, string> {
48+
return {
49+
IMAGE_ERROR: getPhrase("wcf.fancybox.imageError"),
50+
MOVE_UP: getPhrase("wcf.fancybox.moveUp"),
51+
MOVE_DOWN: getPhrase("wcf.fancybox.moveDown"),
52+
MOVE_LEFT: getPhrase("wcf.fancybox.moveLeft"),
53+
MOVE_RIGHT: getPhrase("wcf.fancybox.moveRight"),
54+
ZOOM_IN: getPhrase("wcf.fancybox.zoomIn"),
55+
ZOOM_OUT: getPhrase("wcf.fancybox.zoomOut"),
56+
TOGGLE_FULL: getPhrase("wcf.fancybox.toggleFull"),
57+
TOGGLE_1TO1: getPhrase("wcf.fancybox.toggle1to1"),
58+
ITERATE_ZOOM: getPhrase("wcf.fancybox.iterateZoom"),
59+
ROTATE_CCW: getPhrase("wcf.fancybox.rotateCcw"),
60+
ROTATE_CW: getPhrase("wcf.fancybox.rotateCw"),
61+
FLIP_X: getPhrase("wcf.fancybox.flipX"),
62+
FLIP_Y: getPhrase("wcf.fancybox.flipY"),
63+
RESET: getPhrase("wcf.fancybox.reset"),
64+
ERROR: getPhrase("wcf.fancybox.error"),
65+
GOTO: getPhrase("wcf.fancybox.goto"),
66+
DOWNLOAD: getPhrase("wcf.fancybox.download"),
67+
TOGGLE_EXPAND: getPhrase("wcf.fancybox.toggleExpand"),
68+
TOGGLE_FULLSCREEN: getPhrase("wcf.fancybox.toggleFullscreen"),
69+
TOGGLE_THUMBS: getPhrase("wcf.fancybox.toggleThumbs"),
70+
TOGGLE_AUTOPLAY: getPhrase("wcf.fancybox.toggleAutoplay"),
71+
CLOSE: getPhrase("wcf.fancybox.close"),
72+
NEXT: getPhrase("wcf.fancybox.next"),
73+
PREV: getPhrase("wcf.fancybox.prev"),
74+
MODAL: getPhrase("wcf.fancybox.modal"),
75+
ELEMENT_NOT_FOUND: getPhrase("wcf.fancybox.elementNotFound"),
76+
IFRAME_ERROR: getPhrase("wcf.fancybox.iframeError"),
77+
};
4578
}

ts/WoltLabSuite/Core/Ui/Message/UserConsent.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@ import DomUtil from "../../Dom/Util";
1414
import User from "../../User";
1515

1616
class UserConsent {
17-
private enableAll = false;
18-
1917
constructor() {
2018
if (window.sessionStorage.getItem(`${Core.getStoragePrefix()}user-consent`) === "all") {
21-
this.enableAll = true;
19+
enableAll = true;
2220
}
2321

2422
this.registerEventListeners();
@@ -27,7 +25,7 @@ class UserConsent {
2725
}
2826

2927
private registerEventListeners(): void {
30-
if (this.enableAll) {
28+
if (enableAll) {
3129
this.enableAllExternalMedia();
3230
} else {
3331
wheneverFirstSeen(".jsButtonMessageUserConsentEnable", (button) => {
@@ -39,7 +37,7 @@ class UserConsent {
3937
private click(event: MouseEvent): void {
4038
event.preventDefault();
4139

42-
this.enableAll = true;
40+
enableAll = true;
4341

4442
this.enableAllExternalMedia();
4543

@@ -72,10 +70,15 @@ class UserConsent {
7270
}
7371
}
7472

73+
let enableAll = false;
7574
let userConsent: UserConsent;
7675

7776
export function init(): void {
7877
if (!userConsent) {
7978
userConsent = new UserConsent();
8079
}
8180
}
81+
82+
export function allExternalMediaEnabled(): boolean {
83+
return enableAll;
84+
}

0 commit comments

Comments
 (0)