Browse Source

feat(iot-hub): add PhotoSwipe lightbox for markdown image galleries

- Wire photoswipe (5.4.4) into ui-ngx and load its CSS via angular.json.
- Replace the in-place ".tb-gallery-img-expanded" toggle in
  TbIotHubMarkdownComponent.onReady with a PhotoSwipe lightbox bound
  to each ".tb-gallery-images" container, picking up
  ".tb-gallery-image" anchors. Items are sized from the underlying
  image's naturalWidth/Height; the lightbox slide uses contained
  rendering with a max viewport sized 90vw / 78vh and a 0-opacity
  backdrop.
- Style the gallery: 240px-min auto-fill grid with 12px gap, 115px
  thumbnail strip with object-fit contained images, hover lift
  (border + translate + shadow) and a fade-in caption tooltip
  bottom-pinned over the thumbnail.
pull/15615/head
Igor Kulikov 5 months ago
parent
commit
e0be2025f6
  1. 3
      ui-ngx/angular.json
  2. 1
      ui-ngx/package.json
  3. 64
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss
  4. 87
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts
  5. 5
      ui-ngx/yarn.lock

3
ui-ngx/angular.json

@ -109,7 +109,8 @@
"node_modules/@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css", "node_modules/@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css",
"node_modules/prismjs/themes/prism.css", "node_modules/prismjs/themes/prism.css",
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css", "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css",
"node_modules/ace-diff/dist/ace-diff.min.css" "node_modules/ace-diff/dist/ace-diff.min.css",
"node_modules/photoswipe/dist/photoswipe.css"
], ],
"stylePreprocessorOptions": { "stylePreprocessorOptions": {
"includePaths": [ "includePaths": [

1
ui-ngx/package.json

@ -77,6 +77,7 @@
"ngx-sharebuttons": "^17.0.0", "ngx-sharebuttons": "^17.0.0",
"ngx-translate-messageformat-compiler": "^7.2.0", "ngx-translate-messageformat-compiler": "^7.2.0",
"objectpath": "^2.0.0", "objectpath": "^2.0.0",
"photoswipe": "^5.4.4",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"raphael": "^2.3.0", "raphael": "^2.3.0",
"rxjs": "~7.8.2", "rxjs": "~7.8.2",

64
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss

@ -390,6 +390,70 @@ $table-text-color: rgba(15, 22, 29, 0.8);
} }
} }
.tb-gallery-images {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 12px;
.tb-gallery-image {
position: relative;
cursor: pointer;
border: 2px solid #c1c3c8;
border-radius: 8px;
overflow: hidden;
margin: 0;
padding: 0;
background: #fff;
transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease;
display: block;
&:hover {
border-color: #3d50f5;
transform: translateY(-2px);
box-shadow: 0 6px 18px #0000002e;
outline: none;
.tb-image-tooltip {
opacity: 1;
transform: translateY(0);
}
}
.tb-image-container {
display: block;
height: 115px;
overflow: hidden;
background: #fff;
img {
width: 100%;
max-width: 100%;
height: 100%;
object-fit: contain;
object-position: top center;
display: block;
margin: 0;
padding: 0;
}
}
.tb-image-tooltip {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: .4rem .6rem;
background: #000000b8;
color: #fff;
font-size: .75rem;
line-height: 1.3;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0;
transform: translateY(4px);
transition: opacity .2s ease, transform .2s ease;
pointer-events: none;
}
}
}
.tb-gallery { .tb-gallery {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

87
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts

@ -34,6 +34,8 @@ import {
resolveDocLinkPlaceholders resolveDocLinkPlaceholders
} from '@home/components/iot-hub/iot-hub-markdown.utils'; } from '@home/components/iot-hub/iot-hub-markdown.utils';
import { DevicePackageInfo } from '@shared/models/iot-hub/device-package.models'; import { DevicePackageInfo } from '@shared/models/iot-hub/device-package.models';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
@Component({ @Component({
selector: 'tb-iot-hub-markdown', selector: 'tb-iot-hub-markdown',
@ -97,13 +99,82 @@ export class TbIotHubMarkdownComponent implements OnInit, OnChanges {
onReady() { onReady() {
const container = this.elementRef.nativeElement; const container = this.elementRef.nativeElement;
// Gallery image click-to-expand const galleryImages = container.querySelectorAll<HTMLElement>('.tb-gallery-images');
const galleryImages = container.querySelectorAll('.tb-gallery-img'); const lightbox = new PhotoSwipeLightbox({
galleryImages.forEach(img => { gallery: galleryImages,
img.addEventListener('click', () => { children: '.tb-gallery-image',
img.classList.toggle('tb-gallery-img-expanded'); pswpModule: PhotoSwipe,
counter: false,
bgOpacity: 0
});
lightbox.addFilter('domItemData', (itemData, element) => {
const image = element.querySelector('img');
itemData.src = image.src;
itemData.width = image.naturalWidth;
itemData.height = image.naturalHeight;
itemData.thumbCropped = true;
return itemData;
});
lightbox.on('change', () => {
const item = lightbox.pswp.currSlide.content.element;// element.querySelector('img');
item.style.display = 'block';
item.style.maxWidth = '90vw';
item.style.maxHeight = '78vh';
item.style.objectFit = 'contain';
item.style.borderRadius = '4px';
item.style.boxShadow = '0 20px 60px #00000080';
});
lightbox.on('uiRegister', () => {
lightbox.pswp.element.style.background = '#0a0a148c';
lightbox.pswp.element.style.backdropFilter = 'blur(18px)';
lightbox.pswp.element.style.setProperty('-webkit-backdrop-filter', 'blur(18px)');
lightbox.pswp.ui.registerElement({
name: 'custom-caption',
order: 9,
isButton: false,
appendTo: 'root',
html: '<span class="tb-gallery-caption"></span><span class="tb-gallery-counter"></span>',
onInit: (el, pswp) => {
el.style.position = 'fixed';
el.style.bottom = '1.5rem';
el.style.left = '50%';
el.style.transform = 'translate(-50%)';
el.style.zIndex = '10000';
el.style.display = 'flex';
el.style.flexDirection = 'column';
el.style.alignItems = 'center';
el.style.gap = '.25rem';
el.style.maxWidth = '80vw';
el.style.textAlign = 'center';
el.style.pointerEvents = 'none';
const caption = el.querySelector<HTMLElement>('.tb-gallery-caption');
caption.style.color = '#fff';
caption.style.fontSize = '1.125rem';
caption.style.lineHeight = '1.5';
caption.style.background = '#000000a6';
caption.style.padding = '.5rem 1.25rem';
caption.style.borderRadius = '8px';
caption.style.backdropFilter = 'blur(8px)';
caption.style.setProperty('-webkit-backdrop-filter', 'blur(8px)');
const counter = el.querySelector<HTMLElement>('.tb-gallery-counter');
counter.style.color = '#fff9';
counter.style.fontSize = '.8rem';
lightbox.pswp.on('change', () => {
counter.innerText = pswp.currIndex + 1 + pswp.options.indexIndicatorSep + pswp.getNumItems();
const currSlideElement = lightbox.pswp.currSlide.data.element;
let captionHTML = '';
if (currSlideElement) {
const imageTooltip = currSlideElement.querySelector('.tb-image-tooltip');
if (imageTooltip) {
captionHTML = imageTooltip.innerHTML;
}
}
caption.innerHTML = captionHTML || '';
});
}
}); });
}); });
lightbox.init();
this.ready.emit(container); this.ready.emit(container);
} }
@ -178,9 +249,11 @@ export class TbIotHubMarkdownComponent implements OnInit, OnChanges {
const images = paths const images = paths
.map((p: string) => this.resolveImage(p)) .map((p: string) => this.resolveImage(p))
.filter((src: string | undefined) => !!src) .filter((src: string | undefined) => !!src)
.map((src: string) => `<img src="${src}" alt="" class="tb-gallery-img" />`) .map((src: string) => `<button class="tb-gallery-image"><span class="tb-image-container">
<img src="${src}" alt=""/>
</span><span class="tb-image-tooltip">Placeholder!</span></button>`)
.join(''); .join('');
return `<div class="tb-gallery">${images}</div>`; return `<div class="tb-gallery-images">${images}</div>`;
} }
// Special variables // Special variables

5
ui-ngx/yarn.lock

@ -8742,6 +8742,11 @@ pbf@^3.2.1, pbf@^3.3.0:
ieee754 "^1.1.12" ieee754 "^1.1.12"
resolve-protobuf-schema "^2.1.0" resolve-protobuf-schema "^2.1.0"
photoswipe@^5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.4.tgz#e045dc036453493188d5c8665b0e8f1000ac4d6e"
integrity sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==
picocolors@^1.0.0, picocolors@^1.1.1: picocolors@^1.0.0, picocolors@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"

Loading…
Cancel
Save