feat(dees-mobile-gallery): add mobile gallery component with fullscreen viewer, swipe navigation, action buttons (download/share/delete), PDF support and demo
This commit is contained in:
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-22 - 1.3.0 - feat(dees-mobile-gallery)
|
||||
add mobile gallery component with fullscreen viewer, swipe navigation, action buttons (download/share/delete), PDF support and demo
|
||||
|
||||
- Adds new <dees-mobile-gallery> web component (ts_web/elements/00group-ui/dees-mobile-gallery) with ~650 lines of implementation.
|
||||
- Component features: fullscreen viewing, touch swipe and keyboard navigation, thumbnails, startIndex, minimal mode, and PDF placeholder support.
|
||||
- Action buttons and events: download, share, delete (configurable); emits 'close', 'delete', 'download', and 'share' events.
|
||||
- Provides a static DeesMobileGallery.show(items, config) factory method that returns a Promise with the user action result.
|
||||
- Includes demo (dees-mobile-gallery.demo.ts) with usage examples and exports the component from the 00group-ui index.
|
||||
- Bumps package version from 1.1.0 to 1.2.0 in package.json.
|
||||
|
||||
## 2025-12-22 - 1.1.0 - feat(ui)
|
||||
add mobile context menu and iconbutton components with demos and exports
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog-mobile",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": false,
|
||||
"description": "A mobile-optimized component catalog for building cross-platform business applications with touch-first UI components.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog-mobile',
|
||||
version: '1.1.0',
|
||||
version: '1.3.0',
|
||||
description: 'A mobile-optimized component catalog for building cross-platform business applications with touch-first UI components.'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { injectCssVariables } from '../../00variables.js';
|
||||
import type { DeesMobileGallery } from './dees-mobile-gallery.js';
|
||||
|
||||
export const demoFunc = () => {
|
||||
injectCssVariables();
|
||||
|
||||
// Sample images for demo
|
||||
const sampleImages = [
|
||||
{
|
||||
id: '1',
|
||||
url: 'https://picsum.photos/800/1200?random=1',
|
||||
thumbnailUrl: 'https://picsum.photos/100/100?random=1',
|
||||
filename: 'receipt_001.jpg',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
url: 'https://picsum.photos/1200/800?random=2',
|
||||
thumbnailUrl: 'https://picsum.photos/100/100?random=2',
|
||||
filename: 'shopping_photo.jpg',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
url: 'https://picsum.photos/800/800?random=3',
|
||||
thumbnailUrl: 'https://picsum.photos/100/100?random=3',
|
||||
filename: 'receipt_002.jpg',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
url: 'https://example.com/document.pdf',
|
||||
filename: 'invoice.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<style>
|
||||
.demo-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.demo-section h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--dees-muted-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.demo-note {
|
||||
font-size: 0.875rem;
|
||||
color: var(--dees-muted-foreground);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.demo-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Basic Gallery</h3>
|
||||
<div class="demo-buttons">
|
||||
<dees-mobile-button
|
||||
@click=${() => {
|
||||
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||
gallery.items = sampleImages.slice(0, 3);
|
||||
gallery.config = {
|
||||
showFilename: true,
|
||||
showActions: true,
|
||||
allowDownload: true,
|
||||
allowDelete: false,
|
||||
};
|
||||
gallery.addEventListener('close', () => gallery.remove());
|
||||
gallery.addEventListener('download', (e: CustomEvent) => {
|
||||
console.log('Download requested:', e.detail);
|
||||
});
|
||||
document.body.appendChild(gallery);
|
||||
}}
|
||||
>Open Gallery (3 images)</dees-mobile-button>
|
||||
</div>
|
||||
<p class="demo-note">Fullscreen gallery with swipe navigation on mobile, arrow keys on desktop.</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Single Image</h3>
|
||||
<div class="demo-buttons">
|
||||
<dees-mobile-button
|
||||
variant="outline"
|
||||
@click=${() => {
|
||||
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||
gallery.items = [sampleImages[0]];
|
||||
gallery.config = {
|
||||
showFilename: true,
|
||||
allowDownload: true,
|
||||
};
|
||||
gallery.addEventListener('close', () => gallery.remove());
|
||||
document.body.appendChild(gallery);
|
||||
}}
|
||||
>View Single Image</dees-mobile-button>
|
||||
</div>
|
||||
<p class="demo-note">Single image view without navigation controls.</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>With Delete Action</h3>
|
||||
<div class="demo-buttons">
|
||||
<dees-mobile-button
|
||||
variant="outline"
|
||||
@click=${() => {
|
||||
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||
gallery.items = sampleImages.slice(0, 3);
|
||||
gallery.config = {
|
||||
showFilename: true,
|
||||
allowDownload: true,
|
||||
allowDelete: true,
|
||||
};
|
||||
gallery.addEventListener('close', () => gallery.remove());
|
||||
gallery.addEventListener('delete', (e: CustomEvent) => {
|
||||
console.log('Delete requested:', e.detail);
|
||||
gallery.remove();
|
||||
});
|
||||
document.body.appendChild(gallery);
|
||||
}}
|
||||
>Gallery with Delete</dees-mobile-button>
|
||||
</div>
|
||||
<p class="demo-note">Shows delete action button in red.</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Start at Specific Index</h3>
|
||||
<div class="demo-buttons">
|
||||
<dees-mobile-button
|
||||
variant="outline"
|
||||
@click=${() => {
|
||||
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||
gallery.items = sampleImages.slice(0, 3);
|
||||
gallery.config = {
|
||||
startIndex: 2,
|
||||
showFilename: true,
|
||||
allowDownload: true,
|
||||
};
|
||||
gallery.addEventListener('close', () => gallery.remove());
|
||||
document.body.appendChild(gallery);
|
||||
}}
|
||||
>Open at Image 3</dees-mobile-button>
|
||||
</div>
|
||||
<p class="demo-note">Opens gallery starting at the third image.</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>With PDF</h3>
|
||||
<div class="demo-buttons">
|
||||
<dees-mobile-button
|
||||
variant="outline"
|
||||
@click=${() => {
|
||||
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||
gallery.items = sampleImages;
|
||||
gallery.config = {
|
||||
startIndex: 3,
|
||||
showFilename: true,
|
||||
allowDownload: true,
|
||||
};
|
||||
gallery.addEventListener('close', () => gallery.remove());
|
||||
document.body.appendChild(gallery);
|
||||
}}
|
||||
>Gallery with PDF</dees-mobile-button>
|
||||
</div>
|
||||
<p class="demo-note">PDF files show a placeholder with filename.</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Static Factory Method</h3>
|
||||
<div class="demo-buttons">
|
||||
<dees-mobile-button
|
||||
variant="outline"
|
||||
@click=${async () => {
|
||||
const { DeesMobileGallery } = await import('./dees-mobile-gallery.js');
|
||||
const result = await DeesMobileGallery.show(
|
||||
sampleImages.slice(0, 3),
|
||||
{
|
||||
showFilename: true,
|
||||
allowDownload: true,
|
||||
allowDelete: true,
|
||||
}
|
||||
);
|
||||
console.log('Gallery result:', result);
|
||||
}}
|
||||
>Use Static show()</dees-mobile-button>
|
||||
</div>
|
||||
<p class="demo-note">Uses the static factory method that returns a promise.</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Minimal Mode</h3>
|
||||
<div class="demo-buttons">
|
||||
<dees-mobile-button
|
||||
variant="outline"
|
||||
@click=${() => {
|
||||
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||
gallery.items = sampleImages.slice(0, 3);
|
||||
gallery.config = {
|
||||
showFilename: false,
|
||||
showActions: false,
|
||||
};
|
||||
gallery.addEventListener('close', () => gallery.remove());
|
||||
document.body.appendChild(gallery);
|
||||
}}
|
||||
>Minimal Gallery</dees-mobile-button>
|
||||
</div>
|
||||
<p class="demo-note">No filename or action buttons - just images.</p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
@@ -0,0 +1,650 @@
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
cssManager,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { mobileComponentStyles } from '../../00componentstyles.js';
|
||||
import '../dees-mobile-icon/dees-mobile-icon.js';
|
||||
import { demoFunc } from './dees-mobile-gallery.demo.js';
|
||||
|
||||
export interface IGalleryItem {
|
||||
id: string;
|
||||
url: string;
|
||||
thumbnailUrl?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IGalleryConfig {
|
||||
showFilename?: boolean;
|
||||
showActions?: boolean;
|
||||
allowDelete?: boolean;
|
||||
allowDownload?: boolean;
|
||||
allowShare?: boolean;
|
||||
startIndex?: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-mobile-gallery': DeesMobileGallery;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-mobile-gallery')
|
||||
export class DeesMobileGallery extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor items: IGalleryItem[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
accessor config: IGalleryConfig = {};
|
||||
|
||||
@state()
|
||||
accessor currentIndex: number = 0;
|
||||
|
||||
@state()
|
||||
accessor isLoading: boolean = true;
|
||||
|
||||
@state()
|
||||
accessor showThumbnails: boolean = false;
|
||||
|
||||
// Touch/swipe state
|
||||
private touchStartX: number = 0;
|
||||
private touchStartY: number = 0;
|
||||
private touchDeltaX: number = 0;
|
||||
private isSwiping: boolean = false;
|
||||
private swipeThreshold: number = 50;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
mobileComponentStyles,
|
||||
css`
|
||||
:host {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: var(--dees-z-modal, 500);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
animation: fadeInGallery 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInGallery {
|
||||
to {
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--dees-space-md);
|
||||
padding-top: calc(var(--dees-space-md) + env(safe-area-inset-top, 0px));
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--dees-space-sm);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--dees-radius-full);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background var(--dees-transition-fast);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.filename {
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.counter {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gallery-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
touch-action: pan-y pinch-zoom;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.image-container.swiping {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
.gallery-image.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--dees-radius-full);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background var(--dees-transition-fast), opacity var(--dees-transition-fast);
|
||||
z-index: 5;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-button.prev {
|
||||
left: var(--dees-space-md);
|
||||
}
|
||||
|
||||
.nav-button.next {
|
||||
right: var(--dees-space-md);
|
||||
}
|
||||
|
||||
.nav-button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hide nav buttons on mobile - use swipe instead */
|
||||
@media (max-width: 640px) {
|
||||
.nav-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-footer {
|
||||
padding: var(--dees-space-md);
|
||||
padding-bottom: calc(var(--dees-space-md) + env(safe-area-inset-bottom, 0px));
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--dees-space-lg);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--dees-space-xs);
|
||||
border: none;
|
||||
background: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: var(--dees-space-sm);
|
||||
border-radius: var(--dees-radius-md);
|
||||
transition: background var(--dees-transition-fast);
|
||||
min-width: 64px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.action-button .icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: var(--dees-radius-full);
|
||||
}
|
||||
|
||||
.action-button .label {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.action-button.danger .icon {
|
||||
background: rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
.action-button.danger {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.thumbnails {
|
||||
display: flex;
|
||||
gap: var(--dees-space-xs);
|
||||
justify-content: center;
|
||||
margin-bottom: var(--dees-space-md);
|
||||
overflow-x: auto;
|
||||
padding: 0 var(--dees-space-md);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--dees-radius-sm);
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: opacity var(--dees-transition-fast), transform var(--dees-transition-fast);
|
||||
flex-shrink: 0;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.thumbnail:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.thumbnail.active {
|
||||
opacity: 1;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.pdf-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--dees-space-md);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pdf-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--dees-radius-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-text {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
this.currentIndex = this.config.startIndex ?? 0;
|
||||
document.addEventListener('keydown', this.handleKeydown);
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('keydown', this.handleKeydown);
|
||||
}
|
||||
|
||||
private handleKeydown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
this.handleClose();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
this.goToPrevious();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.goToNext();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private handleClose() {
|
||||
this.dispatchEvent(new CustomEvent('close', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private handleTouchStart = (e: TouchEvent) => {
|
||||
this.touchStartX = e.touches[0].clientX;
|
||||
this.touchStartY = e.touches[0].clientY;
|
||||
this.touchDeltaX = 0;
|
||||
this.isSwiping = false;
|
||||
};
|
||||
|
||||
private handleTouchMove = (e: TouchEvent) => {
|
||||
const deltaX = e.touches[0].clientX - this.touchStartX;
|
||||
const deltaY = e.touches[0].clientY - this.touchStartY;
|
||||
|
||||
// Only swipe horizontally if horizontal movement is greater than vertical
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
|
||||
this.isSwiping = true;
|
||||
this.touchDeltaX = deltaX;
|
||||
this.requestUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
private handleTouchEnd = () => {
|
||||
if (this.isSwiping) {
|
||||
if (this.touchDeltaX > this.swipeThreshold) {
|
||||
this.goToPrevious();
|
||||
} else if (this.touchDeltaX < -this.swipeThreshold) {
|
||||
this.goToNext();
|
||||
}
|
||||
}
|
||||
this.isSwiping = false;
|
||||
this.touchDeltaX = 0;
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private goToPrevious() {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
this.isLoading = true;
|
||||
this.dispatchChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private goToNext() {
|
||||
if (this.currentIndex < this.items.length - 1) {
|
||||
this.currentIndex++;
|
||||
this.isLoading = true;
|
||||
this.dispatchChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private goToIndex(index: number) {
|
||||
if (index >= 0 && index < this.items.length && index !== this.currentIndex) {
|
||||
this.currentIndex = index;
|
||||
this.isLoading = true;
|
||||
this.dispatchChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchChangeEvent() {
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: {
|
||||
index: this.currentIndex,
|
||||
item: this.items[this.currentIndex],
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private handleImageLoad() {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private handleDownload() {
|
||||
const item = this.items[this.currentIndex];
|
||||
this.dispatchEvent(new CustomEvent('download', {
|
||||
detail: item,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private handleDelete() {
|
||||
const item = this.items[this.currentIndex];
|
||||
this.dispatchEvent(new CustomEvent('delete', {
|
||||
detail: item,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private handleShare() {
|
||||
const item = this.items[this.currentIndex];
|
||||
this.dispatchEvent(new CustomEvent('share', {
|
||||
detail: item,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private isPdf(item: IGalleryItem): boolean {
|
||||
return item.mimeType?.toLowerCase() === 'application/pdf' ||
|
||||
item.filename?.toLowerCase().endsWith('.pdf') ||
|
||||
item.url.toLowerCase().endsWith('.pdf');
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const currentItem = this.items[this.currentIndex];
|
||||
const showFilename = this.config.showFilename ?? true;
|
||||
const showActions = this.config.showActions ?? true;
|
||||
const allowDelete = this.config.allowDelete ?? false;
|
||||
const allowDownload = this.config.allowDownload ?? true;
|
||||
const allowShare = this.config.allowShare ?? false;
|
||||
const showThumbnails = this.items.length > 1;
|
||||
|
||||
const swipeTransform = this.isSwiping ? `translateX(${this.touchDeltaX}px)` : '';
|
||||
|
||||
return html`
|
||||
<div class="gallery-header">
|
||||
<div class="header-left">
|
||||
<button class="close-button" @click=${this.handleClose}>
|
||||
<dees-mobile-icon icon="x" size="24"></dees-mobile-icon>
|
||||
</button>
|
||||
${showFilename && currentItem?.filename ? html`
|
||||
<span class="filename">${currentItem.filename}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
${this.items.length > 1 ? html`
|
||||
<span class="counter">${this.currentIndex + 1} / ${this.items.length}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gallery-content"
|
||||
@touchstart=${this.handleTouchStart}
|
||||
@touchmove=${this.handleTouchMove}
|
||||
@touchend=${this.handleTouchEnd}
|
||||
>
|
||||
${this.isLoading ? html`<div class="loading-spinner"></div>` : ''}
|
||||
|
||||
<div
|
||||
class="image-container ${this.isSwiping ? 'swiping' : ''}"
|
||||
style=${swipeTransform ? `transform: ${swipeTransform}` : ''}
|
||||
>
|
||||
${currentItem ? (
|
||||
this.isPdf(currentItem) ? html`
|
||||
<div class="pdf-preview">
|
||||
<div class="pdf-icon">
|
||||
<dees-mobile-icon icon="file-text" size="48" color="white"></dees-mobile-icon>
|
||||
</div>
|
||||
<span class="pdf-text">${currentItem.filename || 'PDF Document'}</span>
|
||||
</div>
|
||||
` : html`
|
||||
<img
|
||||
class="gallery-image ${!this.isLoading ? 'loaded' : ''}"
|
||||
src=${currentItem.url}
|
||||
alt=${currentItem.filename || ''}
|
||||
@load=${this.handleImageLoad}
|
||||
/>
|
||||
`
|
||||
) : ''}
|
||||
</div>
|
||||
|
||||
${this.items.length > 1 ? html`
|
||||
<button
|
||||
class="nav-button prev"
|
||||
@click=${this.goToPrevious}
|
||||
?disabled=${this.currentIndex === 0}
|
||||
>
|
||||
<dees-mobile-icon icon="chevron-left" size="28"></dees-mobile-icon>
|
||||
</button>
|
||||
<button
|
||||
class="nav-button next"
|
||||
@click=${this.goToNext}
|
||||
?disabled=${this.currentIndex === this.items.length - 1}
|
||||
>
|
||||
<dees-mobile-icon icon="chevron-right" size="28"></dees-mobile-icon>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="gallery-footer">
|
||||
${showThumbnails ? html`
|
||||
<div class="thumbnails">
|
||||
${this.items.map((item, index) => html`
|
||||
<img
|
||||
class="thumbnail ${index === this.currentIndex ? 'active' : ''}"
|
||||
src=${item.thumbnailUrl || item.url}
|
||||
alt=""
|
||||
@click=${() => this.goToIndex(index)}
|
||||
/>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${showActions ? html`
|
||||
<div class="actions">
|
||||
${allowDownload ? html`
|
||||
<button class="action-button" @click=${this.handleDownload}>
|
||||
<div class="icon">
|
||||
<dees-mobile-icon icon="download" size="22"></dees-mobile-icon>
|
||||
</div>
|
||||
<span class="label">Download</span>
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
${allowShare ? html`
|
||||
<button class="action-button" @click=${this.handleShare}>
|
||||
<div class="icon">
|
||||
<dees-mobile-icon icon="share" size="22"></dees-mobile-icon>
|
||||
</div>
|
||||
<span class="label">Share</span>
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
${allowDelete ? html`
|
||||
<button class="action-button danger" @click=${this.handleDelete}>
|
||||
<div class="icon">
|
||||
<dees-mobile-icon icon="trash-2" size="22"></dees-mobile-icon>
|
||||
</div>
|
||||
<span class="label">Delete</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to show the gallery
|
||||
*/
|
||||
public static async show(
|
||||
items: IGalleryItem[],
|
||||
config: IGalleryConfig = {}
|
||||
): Promise<{ action: 'close' | 'delete' | 'download' | 'share'; item?: IGalleryItem }> {
|
||||
return new Promise((resolve) => {
|
||||
const gallery = document.createElement('dees-mobile-gallery') as DeesMobileGallery;
|
||||
gallery.items = items;
|
||||
gallery.config = config;
|
||||
|
||||
const cleanup = () => {
|
||||
gallery.remove();
|
||||
};
|
||||
|
||||
gallery.addEventListener('close', () => {
|
||||
cleanup();
|
||||
resolve({ action: 'close' });
|
||||
});
|
||||
|
||||
gallery.addEventListener('delete', (e: CustomEvent) => {
|
||||
cleanup();
|
||||
resolve({ action: 'delete', item: e.detail });
|
||||
});
|
||||
|
||||
gallery.addEventListener('download', (e: CustomEvent) => {
|
||||
// Don't close on download - user might want to download multiple
|
||||
resolve({ action: 'download', item: e.detail });
|
||||
});
|
||||
|
||||
gallery.addEventListener('share', (e: CustomEvent) => {
|
||||
resolve({ action: 'share', item: e.detail });
|
||||
});
|
||||
|
||||
document.body.appendChild(gallery);
|
||||
});
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-ui/dees-mobile-gallery/index.ts
Normal file
1
ts_web/elements/00group-ui/dees-mobile-gallery/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-mobile-gallery.js';
|
||||
@@ -1,6 +1,7 @@
|
||||
// Core UI Components
|
||||
export * from './dees-mobile-button/index.js';
|
||||
export * from './dees-mobile-contextmenu/index.js';
|
||||
export * from './dees-mobile-gallery/index.js';
|
||||
export * from './dees-mobile-icon/index.js';
|
||||
export * from './dees-mobile-iconbutton/index.js';
|
||||
export * from './dees-mobile-header/index.js';
|
||||
|
||||
Reference in New Issue
Block a user