Browse Source
- ${images.gallery(...)} accepts JS-style image objects
({src, alt?, caption?}) that may span multiple lines and contain
whitespace inside the gallery brackets. Captions go through a
whitelist sanitiser (b/strong/i/em/u/s/mark/small/sub/sup/br/code/
span with class+style only — style values containing
expression()/javascript:/url() are dropped) so authors can use
inline markup without exposing XSS.
- Promote escapeHtml / escapeHtmlAttr to exported helpers in
iot-hub-markdown.utils, add the new sanitizeInlineHtml. Drop the
duplicate copies (and the now-unused DocLinks helpers + buildDocLinkButton)
from shared/models/iot-hub/device-package.models.
- Move the PhotoSwipe wiring out of TbIotHubMarkdownComponent into a
reusable tbPhotoSwipeGallery shared directive, declared/exported
by SharedModule. The component now just renders the
data-attributed wrapper and the directive lazy-binds PhotoSwipe.
- Tighten the gallery layout: 200px-min auto-fill grid, 8px vertical
margins, hover border swapped to the shared #2a7dec accent token.
pull/15615/head
6 changed files with 317 additions and 172 deletions
@ -0,0 +1,219 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2026 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// 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 { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import PhotoSwipeLightbox from 'photoswipe/lightbox'; |
||||
|
import PhotoSwipe from 'photoswipe'; |
||||
|
import cssjs from '@core/css/css'; |
||||
|
|
||||
|
const PHOTO_GALLERY_STYLE_ID = 'photoswipe-gallery-style'; |
||||
|
const PHOTO_GALLERY_CLASS = 'tb-photoswipe-gallery'; |
||||
|
const PHOTO_GALLERY_STYLE = |
||||
|
'{\n'+ |
||||
|
' background: rgba(10, 10, 20, 0.55);\n' + |
||||
|
' backdrop-filter: blur(18px);\n' + |
||||
|
' opacity: 1;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__tb-photoswipe-caption {\n' + |
||||
|
' position: fixed;\n' + |
||||
|
' bottom: 1.5rem;\n' + |
||||
|
' left: 50%;\n' + |
||||
|
' transform: translate(-50%);\n' + |
||||
|
' z-index: 10000;\n' + |
||||
|
' display: flex;\n' + |
||||
|
' flex-direction: column;\n' + |
||||
|
' align-items: center;\n' + |
||||
|
' gap: .25rem;\n' + |
||||
|
' max-width: 80vw;\n' + |
||||
|
' text-align: center;\n' + |
||||
|
' pointer-events: none;\n' + |
||||
|
' line-height: 1.75;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__tb-photoswipe-caption .tb-gallery-caption {\n' + |
||||
|
' color: #fff;\n' + |
||||
|
' font-size: 1.125rem;\n' + |
||||
|
' line-height: 1.5;\n' + |
||||
|
' background: #000000a6;\n' + |
||||
|
' padding: .5rem 1.25rem;\n' + |
||||
|
' border-radius: 8px;\n' + |
||||
|
' backdrop-filter: blur(8px);\n' + |
||||
|
' -webkit-backdrop-filter: blur(8px);\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__tb-photoswipe-caption .tb-gallery-counter {\n' + |
||||
|
' color: #fff9;\n' + |
||||
|
' font-size: .8rem;\n' + |
||||
|
'}\n'+ |
||||
|
'\n' + |
||||
|
'.pswp__item img.pswp__img {\n' + |
||||
|
' display: block;\n' + |
||||
|
' max-width: 90vw;\n' + |
||||
|
' max-height: 78vh;\n' + |
||||
|
' object-fit: contain;\n' + |
||||
|
' border-radius: 4px; \n' + |
||||
|
' box-shadow: 0 20px 60px #00000080;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__item .pswp__img--placeholder {\n' + |
||||
|
' border-radius: 4px; \n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button {\n' + |
||||
|
' border-radius: 50%;\n' + |
||||
|
' border: 1px solid rgba(255, 255, 255, .2);\n' + |
||||
|
' background: #1e1e2899;\n' + |
||||
|
' backdrop-filter: blur(8px);\n' + |
||||
|
' -webkit-backdrop-filter: blur(8px);\n' + |
||||
|
' color: #fff;\n' + |
||||
|
' transition: background .18s ease, transform .18s ease;\n' + |
||||
|
' outline: none;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button:hover {\n' + |
||||
|
' background: #3c3c50d9;\n' + |
||||
|
' transform: scale(1.08);\n' + |
||||
|
' outline: none;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button.pswp__button--close {\n' + |
||||
|
' width: 40px;\n' + |
||||
|
' height: 40px;\n' + |
||||
|
' margin-top: 16px;\n' + |
||||
|
' margin-right: 16px;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button.pswp__button--close .pswp__icn {\n' + |
||||
|
' width: 24px;\n' + |
||||
|
' height: 24px;\n' + |
||||
|
' top: 7px;\n' + |
||||
|
' left: 7px;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button.pswp__button--arrow {\n' + |
||||
|
' width: 48px;\n' + |
||||
|
' height: 48px;\n' + |
||||
|
' margin-top: -24px;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button.pswp__button--arrow .pswp__icn {\n' + |
||||
|
' width: 32px;\n' + |
||||
|
' height: 32px;\n' + |
||||
|
' margin-top: 0;\n' + |
||||
|
' top: 7px;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button.pswp__button--arrow.pswp__button--arrow--prev {\n' + |
||||
|
' left: 16px;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button.pswp__button--arrow.pswp__button--arrow--prev .pswp__icn {\n' + |
||||
|
' left: 12px;\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button.pswp__button--arrow.pswp__button--arrow--next {\n' + |
||||
|
' right: 16px\n' + |
||||
|
'}\n' + |
||||
|
'\n' + |
||||
|
'.pswp__button.pswp__button--arrow.pswp__button--arrow--next .pswp__icn { \n' + |
||||
|
' right: 12px;\n' + |
||||
|
'}'; |
||||
|
|
||||
|
@Directive({ |
||||
|
selector: '[tbPhotoSwipeGallery]', |
||||
|
standalone: false |
||||
|
}) |
||||
|
export class PhotoSwipeGalleryDirective implements OnInit, OnDestroy { |
||||
|
|
||||
|
@Input() galleryChildrenSelector = '.tb-image'; |
||||
|
@Input() imageCaptionSelector = '.tb-image-tooltip'; |
||||
|
|
||||
|
private lightbox: PhotoSwipeLightbox; |
||||
|
|
||||
|
constructor( |
||||
|
private elementRef: ElementRef<HTMLElement> |
||||
|
) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.initPhotoSwipeGalleryStyle(); |
||||
|
this.lightbox = new PhotoSwipeLightbox({ |
||||
|
gallery: this.elementRef.nativeElement, |
||||
|
children: this.galleryChildrenSelector, |
||||
|
pswpModule: PhotoSwipe, |
||||
|
counter: false, |
||||
|
bgOpacity: 0, |
||||
|
mainClass: PHOTO_GALLERY_CLASS |
||||
|
}); |
||||
|
this.lightbox.addFilter('domItemData', (itemData, element) => { |
||||
|
let image: HTMLImageElement; |
||||
|
if (element instanceof HTMLImageElement) { |
||||
|
image = element; |
||||
|
} else { |
||||
|
image = element.querySelector('img'); |
||||
|
} |
||||
|
itemData.src = image.src; |
||||
|
itemData.width = image.naturalWidth; |
||||
|
itemData.height = image.naturalHeight; |
||||
|
itemData.thumbCropped = true; |
||||
|
return itemData; |
||||
|
}); |
||||
|
this.lightbox.on('uiRegister', () => { |
||||
|
this.lightbox.pswp.ui.registerElement({ |
||||
|
name: 'tb-photoswipe-caption', |
||||
|
order: 9, |
||||
|
isButton: false, |
||||
|
appendTo: 'root', |
||||
|
html: '<span class="tb-gallery-caption"></span><span class="tb-gallery-counter"></span>', |
||||
|
onInit: (el, pswp) => { |
||||
|
const caption = el.querySelector<HTMLElement>('.tb-gallery-caption'); |
||||
|
const counter = el.querySelector<HTMLElement>('.tb-gallery-counter'); |
||||
|
this.lightbox.pswp.on('change', () => { |
||||
|
counter.innerText = pswp.currIndex + 1 + pswp.options.indexIndicatorSep + pswp.getNumItems(); |
||||
|
const currSlideElement = this.lightbox.pswp.currSlide.data.element; |
||||
|
let imageTooltip: Element; |
||||
|
if (currSlideElement) { |
||||
|
imageTooltip = currSlideElement.querySelector(this.imageCaptionSelector); |
||||
|
} |
||||
|
if (imageTooltip) { |
||||
|
caption.style.display = 'block'; |
||||
|
caption.innerHTML = imageTooltip.innerHTML || ''; |
||||
|
} else { |
||||
|
caption.style.display = 'none'; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
this.lightbox.init(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
if (this.lightbox) { |
||||
|
this.lightbox.destroy(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private initPhotoSwipeGalleryStyle(): void { |
||||
|
const existingElement = document.getElementById(PHOTO_GALLERY_STYLE_ID); |
||||
|
if (!existingElement) { |
||||
|
const cssParser = new cssjs(); |
||||
|
cssParser.testMode = false; |
||||
|
cssParser.cssPreviewNamespace = PHOTO_GALLERY_CLASS; |
||||
|
cssParser.createStyleElement(PHOTO_GALLERY_STYLE_ID, PHOTO_GALLERY_STYLE); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue