ng-images-preview 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -5
- package/fesm2022/ng-images-preview.mjs +682 -192
- package/fesm2022/ng-images-preview.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ng-images-preview.d.ts +163 -13
|
@@ -1,31 +1,137 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input,
|
|
2
|
+
import { input, viewChildren, signal, computed, inject, PLATFORM_ID, effect, HostListener, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, ElementRef, createComponent, Input, Directive, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
|
5
5
|
import { trigger, transition, style, animate } from '@angular/animations';
|
|
6
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
6
7
|
|
|
8
|
+
const PREVIEW_ICONS = {
|
|
9
|
+
close: '<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
|
|
10
|
+
prev: '<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>',
|
|
11
|
+
next: '<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>',
|
|
12
|
+
flipH: '<svg viewBox="0 0 24 24"><path d="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1.9 2 2 2h4v-2H5V5h4V3H5c-1.1 0-2 .9-2 2zm16-2v2h2c0-1.1-.9-2-2-2zm-8 20h2V1h-2v22zm8-6h2v-2h-2v2zM15 5h2V3h-2v2zm4 8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2z"/></svg>',
|
|
13
|
+
flipV: '<svg viewBox="0 0 24 24"><path d="M7 21h2v-2H7v2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-12h2V7h-2v2zm-4 4h2v-4h-2v4zm-8-8h2V3H7v2zm8 0h2V3h-2v2zm-4 8h4v-4h-4v4zM3 15h2v-2H3v2zm12-4h2v-4H3v4h2v-2h10v2zM7 3v2h2V3H7zm8 20h2v-2h-2v2zm-4 0h2v-2h-2v2zm4-12h2v-2h-2v2zM3 19c0 1.1.9 2 2 2h2v-2H5v-2H3v2zm16-6h2v-2h-2v2zm0 4v2c0 1.1-.9 2-2 2h-2v-2h2v-2h2zM5 3c-1.1 0-2 .9-2 2h2V3z"/></svg>',
|
|
14
|
+
rotateLeft: '<svg viewBox="0 0 24 24"><path d="M7.11 8.53L5.7 7.11C4.8 8.27 4.24 9.61 4.07 11h2.02c.14-.87.49-1.72 1.02-2.47zM6.09 13H4.07c.17 1.39.72 2.73 1.62 3.89l1.41-1.42c-.52-.75-.87-1.59-1.01-2.47zm1.01 5.32c1.16.9 2.51 1.44 3.9 1.61V17.9c-.87-.15-1.71-.49-2.46-1.03L7.1 18.32zM13 4.07V1L8.45 5.55 13 10V6.09c2.84.48 5 2.94 5 5.91s-2.16 5.43-5 5.91v2.02c3.95-.49 7-3.85 7-7.93s-3.05-7.44-7-7.93z"/></svg>',
|
|
15
|
+
rotateRight: '<svg viewBox="0 0 24 24"><path d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z"/></svg>',
|
|
16
|
+
zoomOut: '<svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z"/></svg>',
|
|
17
|
+
zoomIn: '<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Image Preview Overlay Component.
|
|
22
|
+
* Displays images in a full-screen overlay with zoom, rotate, and pan capabilities.
|
|
23
|
+
*/
|
|
7
24
|
class ImagesPreviewComponent {
|
|
25
|
+
/**
|
|
26
|
+
* The image source to display.
|
|
27
|
+
* @required
|
|
28
|
+
*/
|
|
8
29
|
src = input.required(...(ngDevMode ? [{ debugName: "src" }] : []));
|
|
30
|
+
/**
|
|
31
|
+
* List of image URLs for gallery navigation.
|
|
32
|
+
*/
|
|
9
33
|
images = input(...(ngDevMode ? [undefined, { debugName: "images" }] : []));
|
|
34
|
+
/**
|
|
35
|
+
* Optional srcset for the single image `src`.
|
|
36
|
+
*/
|
|
37
|
+
srcset = input(...(ngDevMode ? [undefined, { debugName: "srcset" }] : []));
|
|
38
|
+
/**
|
|
39
|
+
* List of srcsets corresponding to the `images` array.
|
|
40
|
+
* Must be 1:1 mapped with `images`.
|
|
41
|
+
*/
|
|
42
|
+
srcsets = input(...(ngDevMode ? [undefined, { debugName: "srcsets" }] : []));
|
|
43
|
+
/**
|
|
44
|
+
* Initial index for gallery navigation.
|
|
45
|
+
* @default 0
|
|
46
|
+
*/
|
|
10
47
|
initialIndex = input(0, ...(ngDevMode ? [{ debugName: "initialIndex" }] : []));
|
|
48
|
+
/**
|
|
49
|
+
* DOMRect of the element that opened the preview.
|
|
50
|
+
* Used for FLIP animation calculation.
|
|
51
|
+
*/
|
|
52
|
+
openerRect = input(undefined, ...(ngDevMode ? [{ debugName: "openerRect" }] : []));
|
|
53
|
+
/**
|
|
54
|
+
* Custom template to render instead of the default viewer.
|
|
55
|
+
* Exposes `ImagesPreviewContext`.
|
|
56
|
+
*/
|
|
11
57
|
customTemplate = input(...(ngDevMode ? [undefined, { debugName: "customTemplate" }] : []));
|
|
58
|
+
/**
|
|
59
|
+
* Configuration for the toolbar buttons.
|
|
60
|
+
*/
|
|
61
|
+
toolbarConfig = input({
|
|
62
|
+
showZoom: true,
|
|
63
|
+
showRotate: true,
|
|
64
|
+
showFlip: true,
|
|
65
|
+
}, ...(ngDevMode ? [{ debugName: "toolbarConfig" }] : []));
|
|
66
|
+
/**
|
|
67
|
+
* Callback function called when the preview is closed.
|
|
68
|
+
*/
|
|
12
69
|
closeCallback = () => {
|
|
13
70
|
/* noop */
|
|
14
71
|
};
|
|
15
|
-
|
|
72
|
+
imgRefs = viewChildren('imgRef', ...(ngDevMode ? [{ debugName: "imgRefs" }] : []));
|
|
73
|
+
thumbRefs = viewChildren('thumbRef', ...(ngDevMode ? [{ debugName: "thumbRefs" }] : []));
|
|
16
74
|
// State signals
|
|
17
75
|
currentIndex = signal(0, ...(ngDevMode ? [{ debugName: "currentIndex" }] : []));
|
|
18
|
-
isLoading = signal(true
|
|
76
|
+
// isLoading = signal(true); // Replaced by per-image tracking
|
|
19
77
|
hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
20
|
-
|
|
78
|
+
// Track loaded state for each image source
|
|
79
|
+
loadedImages = signal(new Set(), ...(ngDevMode ? [{ debugName: "loadedImages" }] : []));
|
|
80
|
+
// Toggle thumbnails
|
|
81
|
+
showThumbnails = input(true, ...(ngDevMode ? [{ debugName: "showThumbnails" }] : []));
|
|
82
|
+
// Toggle toolbar
|
|
83
|
+
showToolbar = input(true, ...(ngDevMode ? [{ debugName: "showToolbar" }] : []));
|
|
84
|
+
// Toolbar custom extensions
|
|
85
|
+
toolbarExtensions = input(null, ...(ngDevMode ? [{ debugName: "toolbarExtensions" }] : []));
|
|
86
|
+
activeBuffer = computed(() => {
|
|
21
87
|
const imgs = this.images();
|
|
22
|
-
|
|
23
|
-
|
|
88
|
+
const srcsets = this.srcsets();
|
|
89
|
+
const current = this.currentIndex();
|
|
90
|
+
// Single image or fallback
|
|
91
|
+
if (!imgs || imgs.length === 0) {
|
|
92
|
+
return [{ src: this.src(), index: 0, offset: 0, srcset: this.srcset() }];
|
|
93
|
+
}
|
|
94
|
+
const buffer = [];
|
|
95
|
+
const total = imgs.length;
|
|
96
|
+
// Helper to get srcset safely
|
|
97
|
+
const getSrcset = (i) => (srcsets && srcsets[i]) ? srcsets[i] : undefined;
|
|
98
|
+
// Prev (-1)
|
|
99
|
+
if (current > 0) {
|
|
100
|
+
buffer.push({
|
|
101
|
+
src: imgs[current - 1],
|
|
102
|
+
index: current - 1,
|
|
103
|
+
offset: -1,
|
|
104
|
+
srcset: getSrcset(current - 1)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Current (0)
|
|
108
|
+
buffer.push({
|
|
109
|
+
src: imgs[current],
|
|
110
|
+
index: current,
|
|
111
|
+
offset: 0,
|
|
112
|
+
srcset: getSrcset(current)
|
|
113
|
+
});
|
|
114
|
+
// Next (+1)
|
|
115
|
+
if (current < total - 1) {
|
|
116
|
+
buffer.push({
|
|
117
|
+
src: imgs[current + 1],
|
|
118
|
+
index: current + 1,
|
|
119
|
+
offset: 1,
|
|
120
|
+
srcset: getSrcset(current + 1)
|
|
121
|
+
});
|
|
24
122
|
}
|
|
25
|
-
return
|
|
26
|
-
}, ...(ngDevMode ? [{ debugName: "
|
|
123
|
+
return buffer;
|
|
124
|
+
}, ...(ngDevMode ? [{ debugName: "activeBuffer" }] : []));
|
|
27
125
|
platformId = inject(PLATFORM_ID);
|
|
126
|
+
sanitizer = inject(DomSanitizer);
|
|
127
|
+
// Icons map with SafeHtml
|
|
128
|
+
icons;
|
|
28
129
|
constructor() {
|
|
130
|
+
// Sanitize icons once
|
|
131
|
+
this.icons = Object.keys(PREVIEW_ICONS).reduce((acc, key) => {
|
|
132
|
+
acc[key] = this.sanitizer.bypassSecurityTrustHtml(PREVIEW_ICONS[key]);
|
|
133
|
+
return acc;
|
|
134
|
+
}, {});
|
|
29
135
|
// defined in class body usually, but here to show logical grouping
|
|
30
136
|
effect(() => {
|
|
31
137
|
// Initialize index from input
|
|
@@ -34,7 +140,6 @@ class ImagesPreviewComponent {
|
|
|
34
140
|
effect(() => {
|
|
35
141
|
// Reset state when index changes
|
|
36
142
|
this.currentIndex();
|
|
37
|
-
this.isLoading.set(true);
|
|
38
143
|
this.hasError.set(false);
|
|
39
144
|
this.reset(); // Reset zoom/rotate
|
|
40
145
|
}, { allowSignalWrites: true });
|
|
@@ -43,19 +148,53 @@ class ImagesPreviewComponent {
|
|
|
43
148
|
const index = this.currentIndex();
|
|
44
149
|
const images = this.images();
|
|
45
150
|
if (isPlatformBrowser(this.platformId) && images && images.length > 0) {
|
|
151
|
+
const srcsets = this.srcsets();
|
|
152
|
+
const getSrcset = (i) => (srcsets && srcsets[i]) ? srcsets[i] : undefined;
|
|
46
153
|
// Next
|
|
47
154
|
if (index < images.length - 1) {
|
|
48
155
|
const img = new Image();
|
|
156
|
+
const s = getSrcset(index + 1);
|
|
157
|
+
if (s)
|
|
158
|
+
img.srcset = s;
|
|
49
159
|
img.src = images[index + 1];
|
|
50
160
|
}
|
|
51
161
|
// Prev
|
|
52
162
|
if (index > 0) {
|
|
53
163
|
const img = new Image();
|
|
164
|
+
const s = getSrcset(index - 1);
|
|
165
|
+
if (s)
|
|
166
|
+
img.srcset = s;
|
|
54
167
|
img.src = images[index - 1];
|
|
55
168
|
}
|
|
56
169
|
}
|
|
57
170
|
});
|
|
171
|
+
effect(() => {
|
|
172
|
+
// Handle FLIP on open
|
|
173
|
+
const opener = this.openerRect();
|
|
174
|
+
if (opener && !this.flipAnimDone) {
|
|
175
|
+
// Defer to let the view render so we know final position
|
|
176
|
+
setTimeout(() => this.runFlipAnimation(opener));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// Auto-scroll thumbnails
|
|
180
|
+
effect(() => {
|
|
181
|
+
const index = this.currentIndex();
|
|
182
|
+
const show = this.showThumbnails();
|
|
183
|
+
const refs = this.thumbRefs();
|
|
184
|
+
if (show && refs && refs[index]) {
|
|
185
|
+
const el = refs[index].nativeElement;
|
|
186
|
+
const container = el.parentElement;
|
|
187
|
+
if (container) {
|
|
188
|
+
const scrollLeft = el.offsetLeft + el.offsetWidth / 2 - container.offsetWidth / 2;
|
|
189
|
+
container.scrollTo({
|
|
190
|
+
left: scrollLeft,
|
|
191
|
+
behavior: 'smooth'
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
58
196
|
}
|
|
197
|
+
flipAnimDone = false;
|
|
59
198
|
scale = signal(1, ...(ngDevMode ? [{ debugName: "scale" }] : []));
|
|
60
199
|
translateX = signal(0, ...(ngDevMode ? [{ debugName: "translateX" }] : []));
|
|
61
200
|
translateY = signal(0, ...(ngDevMode ? [{ debugName: "translateY" }] : []));
|
|
@@ -63,10 +202,11 @@ class ImagesPreviewComponent {
|
|
|
63
202
|
flipH = signal(false, ...(ngDevMode ? [{ debugName: "flipH" }] : []));
|
|
64
203
|
flipV = signal(false, ...(ngDevMode ? [{ debugName: "flipV" }] : []));
|
|
65
204
|
isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
|
|
205
|
+
isInertia = signal(false, ...(ngDevMode ? [{ debugName: "isInertia" }] : []));
|
|
66
206
|
// Touch state
|
|
67
207
|
initialPinchDistance = 0;
|
|
68
208
|
initialScale = 1;
|
|
69
|
-
isPinching = false;
|
|
209
|
+
isPinching = signal(false, ...(ngDevMode ? [{ debugName: "isPinching" }] : []));
|
|
70
210
|
// Computed state object for template
|
|
71
211
|
state = computed(() => ({
|
|
72
212
|
src: this.src(),
|
|
@@ -74,7 +214,7 @@ class ImagesPreviewComponent {
|
|
|
74
214
|
rotate: this.rotate(),
|
|
75
215
|
flipH: this.flipH(),
|
|
76
216
|
flipV: this.flipV(),
|
|
77
|
-
isLoading: this.
|
|
217
|
+
isLoading: !this.loadedImages().has(this.src() || '') && !this.hasError(),
|
|
78
218
|
hasError: this.hasError(),
|
|
79
219
|
}), ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
80
220
|
// Actions object for template
|
|
@@ -102,18 +242,92 @@ class ImagesPreviewComponent {
|
|
|
102
242
|
cachedConstraints = null;
|
|
103
243
|
lastTimestamp = 0;
|
|
104
244
|
rafId = null;
|
|
105
|
-
FRICTION = 0.
|
|
245
|
+
FRICTION = 0.95; // Super smooth glide
|
|
106
246
|
VELOCITY_THRESHOLD = 0.01;
|
|
107
247
|
MAX_VELOCITY = 3; // Cap speed to prevent teleporting
|
|
108
|
-
|
|
248
|
+
// Updated transform logic for slide buffer
|
|
249
|
+
getTransform(offset) {
|
|
250
|
+
const viewportWidth = window.innerWidth;
|
|
251
|
+
const spacing = 16; // Gap between images
|
|
252
|
+
const baseOffset = offset * (viewportWidth + spacing);
|
|
253
|
+
// Dynamic movement from touch/inertia
|
|
254
|
+
// Global translateX applies to the whole "track"
|
|
255
|
+
const x = this.translateX() + baseOffset;
|
|
256
|
+
// Y-axis drag (Pull-to-Close) usually only affects the active image visually,
|
|
257
|
+
// but moving the whole track is acceptable and simpler.
|
|
258
|
+
// Ideally, neighbors stay at Y=0.
|
|
259
|
+
const effectiveY = offset === 0 ? this.translateY() : 0;
|
|
260
|
+
const effectiveScale = offset === 0 ? this.scale() : 1;
|
|
261
|
+
const effectiveRotate = offset === 0 ? this.rotate() : 0;
|
|
262
|
+
const flipH = offset === 0 ? this.flipH() : false;
|
|
263
|
+
const flipV = offset === 0 ? this.flipV() : false;
|
|
264
|
+
const scaleX = flipH ? -1 : 1;
|
|
265
|
+
const scaleY = flipV ? -1 : 1;
|
|
266
|
+
// FLIP override
|
|
267
|
+
if (offset === 0) {
|
|
268
|
+
const flip = this.flipTransform();
|
|
269
|
+
if (flip)
|
|
270
|
+
return flip;
|
|
271
|
+
}
|
|
272
|
+
return `translate3d(${x}px, ${effectiveY}px, 0) scale(${effectiveScale}) rotate(${effectiveRotate}deg) scaleX(${scaleX}) scaleY(${scaleY})`;
|
|
273
|
+
}
|
|
274
|
+
// FLIP State
|
|
275
|
+
flipTransform = signal('', ...(ngDevMode ? [{ debugName: "flipTransform" }] : []));
|
|
276
|
+
// Helper to get current active image element
|
|
277
|
+
getCurrentImageElement() {
|
|
278
|
+
const buffer = this.activeBuffer();
|
|
279
|
+
const index = buffer.findIndex((item) => item.offset === 0);
|
|
280
|
+
if (index === -1)
|
|
281
|
+
return undefined;
|
|
282
|
+
return this.imgRefs()[index]?.nativeElement;
|
|
283
|
+
}
|
|
284
|
+
runFlipAnimation(opener) {
|
|
285
|
+
if (!isPlatformBrowser(this.platformId))
|
|
286
|
+
return;
|
|
287
|
+
const imgEl = this.getCurrentImageElement();
|
|
288
|
+
if (!imgEl)
|
|
289
|
+
return;
|
|
290
|
+
// 1. First: Final state is already rendered (centered, max fit)
|
|
291
|
+
const finalRect = imgEl.getBoundingClientRect();
|
|
292
|
+
// 2. Invert: Calculate scale and translate to match opener
|
|
293
|
+
const scaleX = opener.width / finalRect.width;
|
|
294
|
+
const scaleY = opener.height / finalRect.height;
|
|
295
|
+
// Note: we use the larger scale to crop?
|
|
296
|
+
// Or fit?
|
|
297
|
+
// Usually we want to fit.
|
|
298
|
+
// Let's matching fitting.
|
|
299
|
+
// Actually, most simple FLIP matches dimensions exactly.
|
|
300
|
+
// But aspect ratios might differ.
|
|
301
|
+
// Let's just match the rect exactly.
|
|
302
|
+
const deltaX = opener.left - finalRect.left + (opener.width - finalRect.width) / 2;
|
|
303
|
+
const deltaY = opener.top - finalRect.top + (opener.height - finalRect.height) / 2;
|
|
304
|
+
// Apply Invert Transform immediately (no transition)
|
|
305
|
+
imgEl.style.transition = 'none';
|
|
306
|
+
this.flipTransform.set(`translate3d(${deltaX}px, ${deltaY}px, 0) scale(${scaleX}, ${scaleY})`);
|
|
307
|
+
// Force Reflow
|
|
308
|
+
imgEl.offsetHeight;
|
|
309
|
+
// 3. Play
|
|
310
|
+
requestAnimationFrame(() => {
|
|
311
|
+
// Enable transition
|
|
312
|
+
imgEl.style.transition = 'transform 300ms cubic-bezier(0.2, 0, 0.2, 1)';
|
|
313
|
+
// Remove override (animates to Final)
|
|
314
|
+
this.flipTransform.set('');
|
|
315
|
+
this.flipAnimDone = true;
|
|
316
|
+
// CLEANUP: Remove transition after animation wraps up so dragging/panning is instant
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
imgEl.style.transition = '';
|
|
319
|
+
}, 300);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
overlayBackground = computed(() => {
|
|
323
|
+
const y = Math.abs(this.translateY());
|
|
109
324
|
const scale = this.scale();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}, ...(ngDevMode ? [{ debugName: "transformStyle" }] : []));
|
|
325
|
+
if (scale === 1 && y > 0) {
|
|
326
|
+
const opacity = Math.max(0, 0.95 - y / 400); // Fade out as we drag down
|
|
327
|
+
return `rgba(0, 0, 0, ${opacity})`;
|
|
328
|
+
}
|
|
329
|
+
return 'var(--ng-img-background)';
|
|
330
|
+
}, ...(ngDevMode ? [{ debugName: "overlayBackground" }] : []));
|
|
117
331
|
onEscape() {
|
|
118
332
|
this.close();
|
|
119
333
|
}
|
|
@@ -134,13 +348,19 @@ class ImagesPreviewComponent {
|
|
|
134
348
|
this.translateY.set(nextY);
|
|
135
349
|
return;
|
|
136
350
|
}
|
|
137
|
-
const img = this.
|
|
351
|
+
const img = this.getCurrentImageElement();
|
|
138
352
|
if (!img) {
|
|
139
353
|
this.translateX.set(nextX);
|
|
140
354
|
this.translateY.set(nextY);
|
|
141
355
|
return;
|
|
142
356
|
}
|
|
143
357
|
const scale = this.scale();
|
|
358
|
+
// If scale is 1, we allow free movement for Pull-to-Close and Swipe Nav
|
|
359
|
+
if (scale === 1) {
|
|
360
|
+
this.translateX.set(nextX);
|
|
361
|
+
this.translateY.set(nextY);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
144
364
|
const rotate = Math.abs(this.rotate() % 180);
|
|
145
365
|
const isRotated = rotate === 90;
|
|
146
366
|
const baseWidth = img.offsetWidth;
|
|
@@ -158,7 +378,7 @@ class ImagesPreviewComponent {
|
|
|
158
378
|
this.translateY.set(clampedY);
|
|
159
379
|
}
|
|
160
380
|
onTouchMove(event) {
|
|
161
|
-
if (!this.isDragging() && !this.isPinching)
|
|
381
|
+
if (!this.isDragging() && !this.isPinching())
|
|
162
382
|
return;
|
|
163
383
|
// Prevent default to stop page scrolling/zooming
|
|
164
384
|
if (event.cancelable) {
|
|
@@ -166,7 +386,7 @@ class ImagesPreviewComponent {
|
|
|
166
386
|
}
|
|
167
387
|
const touches = event.touches;
|
|
168
388
|
// One finger: Pan
|
|
169
|
-
if (touches.length === 1 && this.isDragging() && !this.isPinching) {
|
|
389
|
+
if (touches.length === 1 && this.isDragging() && !this.isPinching()) {
|
|
170
390
|
const now = Date.now();
|
|
171
391
|
// Add point to history
|
|
172
392
|
this.touchHistory.push({
|
|
@@ -190,11 +410,30 @@ class ImagesPreviewComponent {
|
|
|
190
410
|
// Apply rubber banding if out of bounds
|
|
191
411
|
// Use cached constraints to avoid reflows
|
|
192
412
|
const constraints = this.cachedConstraints || this.getConstraints();
|
|
193
|
-
|
|
194
|
-
|
|
413
|
+
// At scale 1, we want linear drag for pull-to-close (Y) and swipe (X)
|
|
414
|
+
// But we can keep rubber banding if it feels good.
|
|
415
|
+
// For Pull-to-Close, standard is 1:1 or slightly resisted.
|
|
416
|
+
// Let's stick to the current logic which applies resistance if "out of bounds".
|
|
417
|
+
// Since at scale 1 bounds are 0, it will apply resistance immediately.
|
|
418
|
+
// We want easier pull.
|
|
419
|
+
if (this.scale() === 1) {
|
|
420
|
+
// Reduce resistance
|
|
421
|
+
nextY = currentY + deltaY;
|
|
422
|
+
nextX = currentX + deltaX;
|
|
195
423
|
}
|
|
196
|
-
else
|
|
197
|
-
|
|
424
|
+
else {
|
|
425
|
+
if (nextX > constraints.maxX) {
|
|
426
|
+
nextX = constraints.maxX + (nextX - constraints.maxX) * 0.5;
|
|
427
|
+
}
|
|
428
|
+
else if (nextX < -constraints.maxX) {
|
|
429
|
+
nextX = -constraints.maxX + (nextX + constraints.maxX) * 0.5;
|
|
430
|
+
}
|
|
431
|
+
if (nextY > constraints.maxY) {
|
|
432
|
+
nextY = constraints.maxY + (nextY - constraints.maxY) * 0.5;
|
|
433
|
+
}
|
|
434
|
+
else if (nextY < -constraints.maxY) {
|
|
435
|
+
nextY = -constraints.maxY + (nextY + constraints.maxY) * 0.5;
|
|
436
|
+
}
|
|
198
437
|
}
|
|
199
438
|
if (nextY > constraints.maxY) {
|
|
200
439
|
nextY = constraints.maxY + (nextY - constraints.maxY) * 0.5;
|
|
@@ -208,15 +447,44 @@ class ImagesPreviewComponent {
|
|
|
208
447
|
this.lastTouchY = touches[0].clientY;
|
|
209
448
|
}
|
|
210
449
|
// Two fingers: Pinch Zoom
|
|
211
|
-
if (touches.length === 2) {
|
|
450
|
+
if (touches.length === 2 && this.initialPinchDistance > 0) {
|
|
212
451
|
const distance = this.getDistance(touches);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
452
|
+
const scaleFactor = distance / this.initialPinchDistance;
|
|
453
|
+
// Calculate new scale with limits
|
|
454
|
+
const rawNewScale = this.initialScale * scaleFactor;
|
|
455
|
+
const newScale = Math.min(Math.max(rawNewScale, this.MIN_SCALE), this.MAX_SCALE);
|
|
456
|
+
// Calculate new translation to keep focal point fixed
|
|
457
|
+
const effectiveRatio = newScale / this.initialScale;
|
|
458
|
+
const currentCenter = this.getCenter(touches);
|
|
459
|
+
const cx = currentCenter.x - window.innerWidth / 2;
|
|
460
|
+
const cy = currentCenter.y - window.innerHeight / 2;
|
|
461
|
+
const sx = this.initialPinchCenter.x - window.innerWidth / 2;
|
|
462
|
+
const sy = this.initialPinchCenter.y - window.innerHeight / 2;
|
|
463
|
+
let newTx = cx - (sx - this.initialTranslateX) * effectiveRatio;
|
|
464
|
+
let newTy = cy - (sy - this.initialTranslateY) * effectiveRatio;
|
|
465
|
+
// --- Clamp Logic (Inline for atomicity) ---
|
|
466
|
+
const img = this.getCurrentImageElement();
|
|
467
|
+
if (img) {
|
|
468
|
+
const rotate = Math.abs(this.rotate() % 180);
|
|
469
|
+
const isRotated = rotate === 90;
|
|
470
|
+
const baseWidth = img.offsetWidth;
|
|
471
|
+
const baseHeight = img.offsetHeight;
|
|
472
|
+
const currentWidth = (isRotated ? baseHeight : baseWidth) * newScale;
|
|
473
|
+
const currentHeight = (isRotated ? baseWidth : baseHeight) * newScale;
|
|
474
|
+
const maxTx = Math.max(0, (currentWidth - window.innerWidth) / 2);
|
|
475
|
+
const maxTy = Math.max(0, (currentHeight - window.innerHeight) / 2);
|
|
476
|
+
if (currentWidth <= window.innerWidth)
|
|
477
|
+
newTx = 0;
|
|
478
|
+
else if (Math.abs(newTx) > maxTx)
|
|
479
|
+
newTx = Math.sign(newTx) * maxTx;
|
|
480
|
+
if (currentHeight <= window.innerHeight)
|
|
481
|
+
newTy = 0;
|
|
482
|
+
else if (Math.abs(newTy) > maxTy)
|
|
483
|
+
newTy = Math.sign(newTy) * maxTy;
|
|
219
484
|
}
|
|
485
|
+
this.scale.set(newScale);
|
|
486
|
+
this.translateX.set(newTx);
|
|
487
|
+
this.translateY.set(newTy);
|
|
220
488
|
}
|
|
221
489
|
}
|
|
222
490
|
getConstraints() {
|
|
@@ -226,7 +494,7 @@ class ImagesPreviewComponent {
|
|
|
226
494
|
// The `cachedConstraints` property is used *outside* this method.
|
|
227
495
|
if (!isPlatformBrowser(this.platformId))
|
|
228
496
|
return { maxX: 0, maxY: 0 };
|
|
229
|
-
const img = this.
|
|
497
|
+
const img = this.getCurrentImageElement();
|
|
230
498
|
if (!img)
|
|
231
499
|
return { maxX: 0, maxY: 0 };
|
|
232
500
|
const scale = this.scale();
|
|
@@ -246,7 +514,7 @@ class ImagesPreviewComponent {
|
|
|
246
514
|
clampPosition() {
|
|
247
515
|
if (!isPlatformBrowser(this.platformId))
|
|
248
516
|
return;
|
|
249
|
-
const img = this.
|
|
517
|
+
const img = this.getCurrentImageElement();
|
|
250
518
|
if (!img)
|
|
251
519
|
return;
|
|
252
520
|
const scale = this.scale();
|
|
@@ -283,6 +551,7 @@ class ImagesPreviewComponent {
|
|
|
283
551
|
this.translateY.set(newY);
|
|
284
552
|
}
|
|
285
553
|
startInertia() {
|
|
554
|
+
this.isInertia.set(true); // Disable CSS transition
|
|
286
555
|
let lastTime = Date.now();
|
|
287
556
|
const step = () => {
|
|
288
557
|
if (!this.isDragging() &&
|
|
@@ -292,7 +561,6 @@ class ImagesPreviewComponent {
|
|
|
292
561
|
const dt = Math.min(now - lastTime, 64); // Cap dt to avoid huge jumps if lag
|
|
293
562
|
lastTime = now;
|
|
294
563
|
if (dt === 0) {
|
|
295
|
-
// Skip if 0ms passed (can happen on fast screens)
|
|
296
564
|
this.rafId = requestAnimationFrame(step);
|
|
297
565
|
return;
|
|
298
566
|
}
|
|
@@ -300,14 +568,12 @@ class ImagesPreviewComponent {
|
|
|
300
568
|
this.velocityX = Math.max(-this.MAX_VELOCITY, Math.min(this.MAX_VELOCITY, this.velocityX));
|
|
301
569
|
this.velocityY = Math.max(-this.MAX_VELOCITY, Math.min(this.MAX_VELOCITY, this.velocityY));
|
|
302
570
|
// Time-based friction
|
|
303
|
-
// Standard friction is 0.95 per 16ms frame
|
|
304
571
|
const frictionFactor = Math.pow(this.FRICTION, dt / 16);
|
|
305
572
|
this.velocityX *= frictionFactor;
|
|
306
573
|
this.velocityY *= frictionFactor;
|
|
307
574
|
let nextX = this.translateX() + this.velocityX * dt;
|
|
308
575
|
let nextY = this.translateY() + this.velocityY * dt;
|
|
309
576
|
// Check bounds during inertia
|
|
310
|
-
// Hard stop/bounce logic can be improved here if needed
|
|
311
577
|
const constraints = this.cachedConstraints || this.getConstraints();
|
|
312
578
|
if (nextX > constraints.maxX) {
|
|
313
579
|
nextX = constraints.maxX;
|
|
@@ -331,14 +597,12 @@ class ImagesPreviewComponent {
|
|
|
331
597
|
}
|
|
332
598
|
else {
|
|
333
599
|
this.stopInertia();
|
|
334
|
-
// Clear cache when movement stops
|
|
335
|
-
// The instruction says to clear it in onTouchEnd or reset/scale changes.
|
|
336
|
-
// So, no need to clear it here.
|
|
337
600
|
}
|
|
338
601
|
};
|
|
339
602
|
this.rafId = requestAnimationFrame(step);
|
|
340
603
|
}
|
|
341
604
|
stopInertia() {
|
|
605
|
+
this.isInertia.set(false); // Re-enable CSS transition
|
|
342
606
|
if (this.rafId) {
|
|
343
607
|
cancelAnimationFrame(this.rafId);
|
|
344
608
|
this.rafId = null;
|
|
@@ -380,6 +644,27 @@ class ImagesPreviewComponent {
|
|
|
380
644
|
event.stopPropagation();
|
|
381
645
|
}
|
|
382
646
|
onMouseUp() {
|
|
647
|
+
if (this.isDragging()) {
|
|
648
|
+
// Check Pull to Close for Mouse
|
|
649
|
+
if (this.scale() === 1) {
|
|
650
|
+
const y = this.translateY();
|
|
651
|
+
if (Math.abs(y) > 100) {
|
|
652
|
+
this.close();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
// Reset position if not closed
|
|
656
|
+
if (y !== 0) {
|
|
657
|
+
this.translateY.set(0);
|
|
658
|
+
}
|
|
659
|
+
const x = this.translateX();
|
|
660
|
+
if (x !== 0) {
|
|
661
|
+
this.translateX.set(0);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
this.snapBack();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
383
668
|
this.isDragging.set(false);
|
|
384
669
|
}
|
|
385
670
|
onTouchEnd(event) {
|
|
@@ -405,42 +690,55 @@ class ImagesPreviewComponent {
|
|
|
405
690
|
this.velocityY = 0;
|
|
406
691
|
}
|
|
407
692
|
this.isDragging.set(false);
|
|
408
|
-
this.isPinching
|
|
693
|
+
this.isPinching.set(false);
|
|
409
694
|
this.initialPinchDistance = 0;
|
|
410
695
|
// Swipe Navigation (at 1x scale)
|
|
411
696
|
if (this.scale() === 1) {
|
|
412
697
|
const x = this.translateX();
|
|
413
|
-
const
|
|
698
|
+
const y = this.translateY();
|
|
699
|
+
// Pull to Close
|
|
700
|
+
if (Math.abs(y) > 100) {
|
|
701
|
+
this.close();
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const threshold = window.innerWidth * 0.25;
|
|
705
|
+
const images = this.images();
|
|
706
|
+
const total = images ? images.length : 0;
|
|
707
|
+
const index = this.currentIndex();
|
|
414
708
|
if (x < -threshold) {
|
|
415
|
-
|
|
709
|
+
// NEXT (Slide Left)
|
|
710
|
+
if (index < total - 1) {
|
|
711
|
+
this.animateSlide(-1);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
this.translateX.set(0);
|
|
715
|
+
this.translateY.set(0);
|
|
716
|
+
}
|
|
416
717
|
return;
|
|
417
718
|
}
|
|
418
719
|
else if (x > threshold) {
|
|
419
|
-
|
|
720
|
+
// PREV (Slide Right)
|
|
721
|
+
if (index > 0) {
|
|
722
|
+
this.animateSlide(1);
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
this.translateX.set(0);
|
|
726
|
+
this.translateY.set(0);
|
|
727
|
+
}
|
|
420
728
|
return;
|
|
421
729
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const x = this.translateX();
|
|
426
|
-
const y = this.translateY();
|
|
427
|
-
const outOfBounds = x > constraints.maxX ||
|
|
428
|
-
x < -constraints.maxX ||
|
|
429
|
-
y > constraints.maxY ||
|
|
430
|
-
y < -constraints.maxY;
|
|
431
|
-
if (outOfBounds) {
|
|
432
|
-
this.snapBack();
|
|
433
|
-
this.velocityX = 0;
|
|
434
|
-
this.velocityY = 0;
|
|
435
|
-
this.cachedConstraints = null;
|
|
730
|
+
// Snap back
|
|
731
|
+
this.translateX.set(0);
|
|
732
|
+
this.translateY.set(0);
|
|
436
733
|
}
|
|
437
734
|
else {
|
|
438
735
|
this.startInertia();
|
|
439
736
|
}
|
|
737
|
+
return;
|
|
440
738
|
}
|
|
441
|
-
else if (touches.length === 1 && this.isPinching) {
|
|
739
|
+
else if (touches.length === 1 && this.isPinching()) {
|
|
442
740
|
// Transition from pinch to pan
|
|
443
|
-
this.isPinching
|
|
741
|
+
this.isPinching.set(false);
|
|
444
742
|
this.isDragging.set(true);
|
|
445
743
|
this.lastTouchX = touches[0].clientX;
|
|
446
744
|
this.lastTouchY = touches[0].clientY;
|
|
@@ -454,18 +752,32 @@ class ImagesPreviewComponent {
|
|
|
454
752
|
}
|
|
455
753
|
lastTouchX = 0;
|
|
456
754
|
lastTouchY = 0;
|
|
755
|
+
initialPinchCenter = { x: 0, y: 0 };
|
|
756
|
+
initialTranslateX = 0;
|
|
757
|
+
initialTranslateY = 0;
|
|
457
758
|
getDistance(touches) {
|
|
458
759
|
return Math.hypot(touches[0].clientX - touches[1].clientX, touches[0].clientY - touches[1].clientY);
|
|
459
760
|
}
|
|
761
|
+
getCenter(touches) {
|
|
762
|
+
return {
|
|
763
|
+
x: (touches[0].clientX + touches[1].clientX) / 2,
|
|
764
|
+
y: (touches[0].clientY + touches[1].clientY) / 2,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
460
767
|
close() {
|
|
461
768
|
this.closeCallback();
|
|
462
769
|
}
|
|
463
|
-
onImageLoad() {
|
|
464
|
-
this.
|
|
770
|
+
onImageLoad(src) {
|
|
771
|
+
this.loadedImages.update(set => {
|
|
772
|
+
const newSet = new Set(set);
|
|
773
|
+
newSet.add(src);
|
|
774
|
+
return newSet;
|
|
775
|
+
});
|
|
465
776
|
this.hasError.set(false);
|
|
466
777
|
}
|
|
467
|
-
onImageError() {
|
|
468
|
-
this.
|
|
778
|
+
onImageError(src) {
|
|
779
|
+
// this.loadedImages.update... (maybe not mark as loaded? or separate error tracking)
|
|
780
|
+
// For now, keep error simple
|
|
469
781
|
this.hasError.set(true);
|
|
470
782
|
}
|
|
471
783
|
// Zoom
|
|
@@ -521,14 +833,45 @@ class ImagesPreviewComponent {
|
|
|
521
833
|
this.currentIndex.update((i) => i - 1);
|
|
522
834
|
}
|
|
523
835
|
}
|
|
836
|
+
jumpTo(index) {
|
|
837
|
+
this.currentIndex.set(index);
|
|
838
|
+
this.reset();
|
|
839
|
+
}
|
|
840
|
+
// Slide Animation
|
|
841
|
+
animateSlide(direction) {
|
|
842
|
+
const spacing = 16;
|
|
843
|
+
const width = window.innerWidth + spacing;
|
|
844
|
+
// direction -1 (Next) -> move to -width
|
|
845
|
+
// direction 1 (Prev) -> move to width
|
|
846
|
+
// Wait. If I swipe Left (Next), x is negative. Target should be -width.
|
|
847
|
+
// If I swipe Right (Prev), x is positive. Target should be width.
|
|
848
|
+
// So target = direction * width? No.
|
|
849
|
+
// If direction is "Next" (index + 1), I want to slide to the Left (-X).
|
|
850
|
+
// Let's pass the sign of movement explicitly.
|
|
851
|
+
const target = direction === -1 ? -width : width;
|
|
852
|
+
this.translateX.set(target);
|
|
853
|
+
// Wait for CSS transition (0.3s)
|
|
854
|
+
setTimeout(() => {
|
|
855
|
+
// Update Index (silent swap)
|
|
856
|
+
this.isInertia.set(true); // Disable transition
|
|
857
|
+
if (direction === -1)
|
|
858
|
+
this.next();
|
|
859
|
+
else
|
|
860
|
+
this.prev();
|
|
861
|
+
this.translateX.set(0);
|
|
862
|
+
// Re-enable transition
|
|
863
|
+
setTimeout(() => {
|
|
864
|
+
this.isInertia.set(false);
|
|
865
|
+
}, 50);
|
|
866
|
+
}, 300);
|
|
867
|
+
}
|
|
524
868
|
// Mouse Interaction
|
|
525
869
|
onMouseDown(event) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
if (this.scale() <= 1)
|
|
870
|
+
// Allow dragging even at scale 1 for Pull-to-Close and Swipe-like nav (if intended)
|
|
871
|
+
if (this.isDragging())
|
|
872
|
+
return;
|
|
873
|
+
// Only trigger on Left Click
|
|
874
|
+
if (event.button !== 0)
|
|
532
875
|
return;
|
|
533
876
|
this.isDragging.set(true);
|
|
534
877
|
this.startX = event.clientX;
|
|
@@ -540,36 +883,38 @@ class ImagesPreviewComponent {
|
|
|
540
883
|
// Touch Interaction (bound in template)
|
|
541
884
|
onTouchStart(event) {
|
|
542
885
|
this.stopInertia(); // Cancel any ongoing movement
|
|
886
|
+
// if (event.cancelable) event.preventDefault(); // Removed to restore Tap-to-Close (click events)
|
|
887
|
+
// touch-action: none in CSS handles scroll prevention.
|
|
543
888
|
const touches = event.touches;
|
|
544
889
|
if (touches.length === 1) {
|
|
545
|
-
// Single touch: Pan.
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
// Cache layout to prevent thrashing
|
|
564
|
-
this.cachedConstraints = this.getConstraints();
|
|
565
|
-
}
|
|
890
|
+
// Single touch: Pan.
|
|
891
|
+
// Allow dragging from anywhere in the container (better UX for Pull-to-Close)
|
|
892
|
+
this.isDragging.set(true);
|
|
893
|
+
this.lastTouchX = touches[0].clientX;
|
|
894
|
+
this.lastTouchY = touches[0].clientY;
|
|
895
|
+
this.lastTimestamp = Date.now();
|
|
896
|
+
// Initialize physics state
|
|
897
|
+
this.velocityX = 0;
|
|
898
|
+
this.velocityY = 0;
|
|
899
|
+
this.touchHistory = [
|
|
900
|
+
{
|
|
901
|
+
x: touches[0].clientX,
|
|
902
|
+
y: touches[0].clientY,
|
|
903
|
+
time: Date.now(),
|
|
904
|
+
},
|
|
905
|
+
];
|
|
906
|
+
// Cache layout to prevent thrashing
|
|
907
|
+
this.cachedConstraints = this.getConstraints();
|
|
566
908
|
}
|
|
567
909
|
else if (touches.length === 2) {
|
|
568
910
|
// Two fingers: Pinch
|
|
569
|
-
this.isPinching
|
|
911
|
+
this.isPinching.set(true);
|
|
570
912
|
this.isDragging.set(false); // Stop panning
|
|
571
913
|
this.initialPinchDistance = this.getDistance(touches);
|
|
572
914
|
this.initialScale = this.scale();
|
|
915
|
+
this.initialPinchCenter = this.getCenter(touches);
|
|
916
|
+
this.initialTranslateX = this.translateX();
|
|
917
|
+
this.initialTranslateY = this.translateY();
|
|
573
918
|
// Clear caching on pinch (scale changes will invalidate limits)
|
|
574
919
|
this.cachedConstraints = null;
|
|
575
920
|
// Prevent default to avoid browser zoom
|
|
@@ -578,7 +923,7 @@ class ImagesPreviewComponent {
|
|
|
578
923
|
}
|
|
579
924
|
}
|
|
580
925
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: ImagesPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
581
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.7", type: ImagesPreviewComponent, isStandalone: true, selector: "ng-images-preview", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: false, transformFunction: null }, initialIndex: { classPropertyName: "initialIndex", publicName: "initialIndex", isSignal: true, isRequired: false, transformFunction: null }, customTemplate: { classPropertyName: "customTemplate", publicName: "customTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:mouseup": "onMouseUp()", "document:touchmove": "onTouchMove($event)", "document:touchend": "onTouchEnd($event)", "document:keydown.arrowleft": "prev()", "document:keydown.arrowright": "next()", "document:keydown.escape": "onEscape()", "document:mousemove": "onMouseMove($event)" } }, viewQueries: [{ propertyName: "imgRef",
|
|
926
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.7", type: ImagesPreviewComponent, isStandalone: true, selector: "ng-images-preview", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: false, transformFunction: null }, srcset: { classPropertyName: "srcset", publicName: "srcset", isSignal: true, isRequired: false, transformFunction: null }, srcsets: { classPropertyName: "srcsets", publicName: "srcsets", isSignal: true, isRequired: false, transformFunction: null }, initialIndex: { classPropertyName: "initialIndex", publicName: "initialIndex", isSignal: true, isRequired: false, transformFunction: null }, openerRect: { classPropertyName: "openerRect", publicName: "openerRect", isSignal: true, isRequired: false, transformFunction: null }, customTemplate: { classPropertyName: "customTemplate", publicName: "customTemplate", isSignal: true, isRequired: false, transformFunction: null }, toolbarConfig: { classPropertyName: "toolbarConfig", publicName: "toolbarConfig", isSignal: true, isRequired: false, transformFunction: null }, showThumbnails: { classPropertyName: "showThumbnails", publicName: "showThumbnails", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, toolbarExtensions: { classPropertyName: "toolbarExtensions", publicName: "toolbarExtensions", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:mouseup": "onMouseUp()", "document:touchmove": "onTouchMove($event)", "document:touchend": "onTouchEnd($event)", "document:keydown.arrowleft": "prev()", "document:keydown.arrowright": "next()", "document:keydown.escape": "onEscape()", "document:mousemove": "onMouseMove($event)" } }, viewQueries: [{ propertyName: "imgRefs", predicate: ["imgRef"], descendants: true, isSignal: true }, { propertyName: "thumbRefs", predicate: ["thumbRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
582
927
|
<div
|
|
583
928
|
class="overlay"
|
|
584
929
|
[@fadeInOut]
|
|
@@ -587,6 +932,7 @@ class ImagesPreviewComponent {
|
|
|
587
932
|
tabindex="0"
|
|
588
933
|
role="button"
|
|
589
934
|
aria-label="Close preview overlay"
|
|
935
|
+
[style.background-color]="overlayBackground()"
|
|
590
936
|
>
|
|
591
937
|
<!-- Custom Template Support -->
|
|
592
938
|
@if (customTemplate(); as template) {
|
|
@@ -595,7 +941,7 @@ class ImagesPreviewComponent {
|
|
|
595
941
|
></ng-container>
|
|
596
942
|
} @else {
|
|
597
943
|
<!-- Loading -->
|
|
598
|
-
@if (
|
|
944
|
+
@if (!loadedImages().has(src()) && !hasError()) {
|
|
599
945
|
<div class="loader">Loading...</div>
|
|
600
946
|
}
|
|
601
947
|
|
|
@@ -611,27 +957,32 @@ class ImagesPreviewComponent {
|
|
|
611
957
|
(touchstart)="onTouchStart($event)"
|
|
612
958
|
tabindex="-1"
|
|
613
959
|
>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
960
|
+
@for (item of activeBuffer(); track item.src) {
|
|
961
|
+
<img
|
|
962
|
+
#imgRef
|
|
963
|
+
[src]="item.src"
|
|
964
|
+
[srcset]="item.srcset || ''"
|
|
965
|
+
[class.opacity-0]="item.offset === 0 && (!loadedImages().has(item.src) || hasError())"
|
|
966
|
+
class="preview-image"
|
|
967
|
+
[class.dragging]="isDragging()"
|
|
968
|
+
[class.inertia]="isInertia()"
|
|
969
|
+
[class.pinching]="isPinching()"
|
|
970
|
+
[class.zoom-in]="scale() === 1"
|
|
971
|
+
[class.zoom-out]="scale() > 1"
|
|
972
|
+
[class.hidden]="item.offset !== 0 && scale() > 1"
|
|
973
|
+
[style.transform]="getTransform(item.offset)"
|
|
974
|
+
(load)="onImageLoad(item.src)"
|
|
975
|
+
(error)="onImageError(item.src)"
|
|
976
|
+
(mousedown)="onMouseDown($event)"
|
|
977
|
+
(click)="$event.stopPropagation()"
|
|
978
|
+
draggable="false"
|
|
979
|
+
alt="Preview"
|
|
980
|
+
/>
|
|
981
|
+
}
|
|
630
982
|
</div>
|
|
631
983
|
|
|
632
984
|
<!-- Close Button -->
|
|
633
|
-
<button class="close-btn" (click)="close()" aria-label="Close preview">
|
|
634
|
-
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
|
985
|
+
<button class="close-btn" (click)="close()" aria-label="Close preview" [innerHTML]="icons.close">
|
|
635
986
|
</button>
|
|
636
987
|
|
|
637
988
|
<!-- Navigation -->
|
|
@@ -643,8 +994,8 @@ class ImagesPreviewComponent {
|
|
|
643
994
|
(click)="prev(); $event.stopPropagation()"
|
|
644
995
|
(mousedown)="$event.stopPropagation()"
|
|
645
996
|
aria-label="Previous image"
|
|
997
|
+
[innerHTML]="icons.prev"
|
|
646
998
|
>
|
|
647
|
-
<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
|
|
648
999
|
</button>
|
|
649
1000
|
}
|
|
650
1001
|
<!-- Check if not last -->
|
|
@@ -654,8 +1005,8 @@ class ImagesPreviewComponent {
|
|
|
654
1005
|
(click)="next(); $event.stopPropagation()"
|
|
655
1006
|
(mousedown)="$event.stopPropagation()"
|
|
656
1007
|
aria-label="Next image"
|
|
1008
|
+
[innerHTML]="icons.next"
|
|
657
1009
|
>
|
|
658
|
-
<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
|
659
1010
|
</button>
|
|
660
1011
|
}
|
|
661
1012
|
|
|
@@ -664,40 +1015,61 @@ class ImagesPreviewComponent {
|
|
|
664
1015
|
}
|
|
665
1016
|
|
|
666
1017
|
<!-- Toolbar -->
|
|
1018
|
+
<!-- Toolbar -->
|
|
1019
|
+
@if (showToolbar()) {
|
|
667
1020
|
<div
|
|
668
1021
|
class="toolbar"
|
|
669
1022
|
(click)="$event.stopPropagation()"
|
|
670
1023
|
(keydown)="onToolbarKey($event)"
|
|
671
1024
|
tabindex="0"
|
|
672
1025
|
>
|
|
673
|
-
<!-- Flip
|
|
674
|
-
|
|
675
|
-
<
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
<!--
|
|
682
|
-
|
|
683
|
-
<
|
|
684
|
-
|
|
685
|
-
<!-- Rotate
|
|
686
|
-
|
|
687
|
-
<
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1026
|
+
<!-- Flip -->
|
|
1027
|
+
@if (toolbarConfig().showFlip) {
|
|
1028
|
+
<button class="toolbar-btn" (click)="flipHorizontal()" aria-label="Flip Horizontal" [innerHTML]="icons.flipH">
|
|
1029
|
+
</button>
|
|
1030
|
+
<button class="toolbar-btn" (click)="flipVertical()" aria-label="Flip Vertical" [innerHTML]="icons.flipV">
|
|
1031
|
+
</button>
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
<!-- Extensions (Custom Buttons) -->
|
|
1035
|
+
@if (toolbarExtensions()) {
|
|
1036
|
+
<ng-container *ngTemplateOutlet="toolbarExtensions(); context: { $implicit: images()![currentIndex()] }"></ng-container>
|
|
1037
|
+
}
|
|
1038
|
+
<!-- Rotate -->
|
|
1039
|
+
@if (toolbarConfig().showRotate) {
|
|
1040
|
+
<button class="toolbar-btn" (click)="rotateLeft()" aria-label="Rotate Left" [innerHTML]="icons.rotateLeft">
|
|
1041
|
+
</button>
|
|
1042
|
+
<button class="toolbar-btn" (click)="rotateRight()" aria-label="Rotate Right" [innerHTML]="icons.rotateRight">
|
|
1043
|
+
</button>
|
|
1044
|
+
}
|
|
1045
|
+
<!-- Zoom -->
|
|
1046
|
+
@if (toolbarConfig().showZoom) {
|
|
1047
|
+
<button class="toolbar-btn" (click)="zoomOut()" aria-label="Zoom Out" [innerHTML]="icons.zoomOut">
|
|
1048
|
+
</button>
|
|
1049
|
+
<button class="toolbar-btn" (click)="zoomIn()" aria-label="Zoom In" [innerHTML]="icons.zoomIn">
|
|
1050
|
+
</button>
|
|
1051
|
+
}
|
|
1052
|
+
</div>
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
<!-- Thumbnails -->
|
|
1057
|
+
@if (images() && images()!.length > 1 && showThumbnails()) {
|
|
1058
|
+
<div class="thumbnail-strip" (click)="$event.stopPropagation()">
|
|
1059
|
+
@for (img of images(); track img; let i = $index) {
|
|
1060
|
+
<div
|
|
1061
|
+
#thumbRef
|
|
1062
|
+
class="thumbnail-item"
|
|
1063
|
+
[class.active]="i === currentIndex()"
|
|
1064
|
+
(click)="jumpTo(i)"
|
|
1065
|
+
>
|
|
1066
|
+
<img [src]="img" loading="lazy" alt="Thumbnail">
|
|
1067
|
+
</div>
|
|
1068
|
+
}
|
|
697
1069
|
</div>
|
|
698
1070
|
}
|
|
699
1071
|
</div>
|
|
700
|
-
`, isInline: true, styles: [":host{display:block}.overlay{position:fixed;inset:0;z-index:
|
|
1072
|
+
`, isInline: true, styles: [":host{display:block;--ng-img-background: rgba(0, 0, 0, .95);--ng-img-text-color: rgba(255, 255, 255, .8);--ng-img-z-index: 50;--ng-img-toolbar-bg: rgba(255, 255, 255, .1);--ng-img-toolbar-hover: rgba(255, 255, 255, .2);--ng-img-gap: 16px;--ng-img-item-bg: rgba(0, 0, 0, .3);--ng-img-thumb-strip-bg: rgba(0, 0, 0, .4);--ng-img-thumb-width: 60px;--ng-img-thumb-height: 40px;--ng-img-thumb-gap: 8px;--ng-img-thumb-border-radius: 6px;--ng-img-thumb-active-border: white}.overlay{position:fixed;inset:0;z-index:var(--ng-img-z-index);display:flex;align-items:center;justify-content:center;background-color:var(--ng-img-background);overflow:hidden;-webkit-user-select:none;user-select:none;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;touch-action:none}.loader,.error{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--ng-img-text-color);font-size:16px}.image-container{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;box-sizing:border-box;display:flex;align-items:center;justify-content:center;touch-action:none}.preview-image{position:absolute;top:0;left:0;width:100%;height:100%;margin:auto;object-fit:contain;pointer-events:auto;will-change:transform;transform-origin:center center;transition:transform .3s cubic-bezier(.2,0,.2,1);touch-action:none}.preview-image.dragging,.preview-image.inertia,.preview-image.pinching{cursor:move;transition:none}.preview-image.zoom-in{cursor:zoom-in}.preview-image.zoom-out{cursor:grab}.toolbar{position:absolute;bottom:30px;left:50%;transform:translate(-50%);display:flex;gap:var(--ng-img-gap);background-color:var(--ng-img-toolbar-bg);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:10px 20px;border-radius:24px;pointer-events:auto;z-index:calc(var(--ng-img-z-index) + 10)}.toolbar-btn{background:none;border:none;color:#fff;cursor:pointer;padding:8px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.toolbar-btn:hover{background-color:var(--ng-img-toolbar-hover)}.toolbar-btn:focus-visible{outline:2px solid white;outline-offset:2px}.toolbar-btn ::ng-deep svg{width:24px;height:24px;fill:currentColor}.close-btn{position:absolute;top:20px;right:20px;z-index:calc(var(--ng-img-z-index) + 10);background:none;border:none;color:#fff;cursor:pointer;padding:8px;border-radius:50%;background-color:#00000080;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.close-btn ::ng-deep svg{width:24px;height:24px;fill:currentColor}.close-btn:hover{background:var(--ng-img-toolbar-hover);transform:rotate(90deg)}.nav-btn{position:absolute;top:50%;transform:translateY(-50%);width:48px;height:48px;border-radius:50%;background:var(--ng-img-toolbar-bg);border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;z-index:10}.nav-btn:hover{background:var(--ng-img-toolbar-hover)}.nav-btn.prev{left:20px}.nav-btn.next{right:20px}.nav-btn ::ng-deep svg{width:32px;height:32px;fill:currentColor;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.counter{position:absolute;top:20px;left:50%;transform:translate(-50%);color:var(--ng-img-text-color);font-size:14px;background:var(--ng-img-item-bg);padding:4px 12px;border-radius:12px;pointer-events:none;z-index:10}.hidden{display:none}.thumbnail-strip{position:absolute;bottom:100px;left:50%;transform:translate(-50%);display:inline-block;white-space:nowrap;max-width:90%;width:auto;overflow-x:auto;overflow-y:hidden;padding:8px;background:var(--ng-img-thumb-strip-bg);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:12px;scroll-behavior:smooth;scrollbar-width:none;z-index:55;font-size:0}.thumbnail-strip::-webkit-scrollbar{display:none}.thumbnail-item{display:inline-block;vertical-align:middle;width:var(--ng-img-thumb-width);height:var(--ng-img-thumb-height);margin-right:var(--ng-img-thumb-gap);border-radius:var(--ng-img-thumb-border-radius);overflow:hidden;cursor:pointer;opacity:.6;transition:all .2s;border:2px solid transparent;box-sizing:border-box}.thumbnail-item:last-child{margin-right:0}.thumbnail-item:hover{opacity:.8}.thumbnail-item.active{opacity:1;border-color:var(--ng-img-thumb-active-border);transform:scale(1.1)}.thumbnail-item img{width:100%;height:100%;object-fit:cover}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], animations: [
|
|
701
1073
|
trigger('fadeInOut', [
|
|
702
1074
|
transition(':enter', [
|
|
703
1075
|
style({ opacity: 0 }),
|
|
@@ -718,6 +1090,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
718
1090
|
tabindex="0"
|
|
719
1091
|
role="button"
|
|
720
1092
|
aria-label="Close preview overlay"
|
|
1093
|
+
[style.background-color]="overlayBackground()"
|
|
721
1094
|
>
|
|
722
1095
|
<!-- Custom Template Support -->
|
|
723
1096
|
@if (customTemplate(); as template) {
|
|
@@ -726,7 +1099,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
726
1099
|
></ng-container>
|
|
727
1100
|
} @else {
|
|
728
1101
|
<!-- Loading -->
|
|
729
|
-
@if (
|
|
1102
|
+
@if (!loadedImages().has(src()) && !hasError()) {
|
|
730
1103
|
<div class="loader">Loading...</div>
|
|
731
1104
|
}
|
|
732
1105
|
|
|
@@ -742,27 +1115,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
742
1115
|
(touchstart)="onTouchStart($event)"
|
|
743
1116
|
tabindex="-1"
|
|
744
1117
|
>
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1118
|
+
@for (item of activeBuffer(); track item.src) {
|
|
1119
|
+
<img
|
|
1120
|
+
#imgRef
|
|
1121
|
+
[src]="item.src"
|
|
1122
|
+
[srcset]="item.srcset || ''"
|
|
1123
|
+
[class.opacity-0]="item.offset === 0 && (!loadedImages().has(item.src) || hasError())"
|
|
1124
|
+
class="preview-image"
|
|
1125
|
+
[class.dragging]="isDragging()"
|
|
1126
|
+
[class.inertia]="isInertia()"
|
|
1127
|
+
[class.pinching]="isPinching()"
|
|
1128
|
+
[class.zoom-in]="scale() === 1"
|
|
1129
|
+
[class.zoom-out]="scale() > 1"
|
|
1130
|
+
[class.hidden]="item.offset !== 0 && scale() > 1"
|
|
1131
|
+
[style.transform]="getTransform(item.offset)"
|
|
1132
|
+
(load)="onImageLoad(item.src)"
|
|
1133
|
+
(error)="onImageError(item.src)"
|
|
1134
|
+
(mousedown)="onMouseDown($event)"
|
|
1135
|
+
(click)="$event.stopPropagation()"
|
|
1136
|
+
draggable="false"
|
|
1137
|
+
alt="Preview"
|
|
1138
|
+
/>
|
|
1139
|
+
}
|
|
761
1140
|
</div>
|
|
762
1141
|
|
|
763
1142
|
<!-- Close Button -->
|
|
764
|
-
<button class="close-btn" (click)="close()" aria-label="Close preview">
|
|
765
|
-
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
|
1143
|
+
<button class="close-btn" (click)="close()" aria-label="Close preview" [innerHTML]="icons.close">
|
|
766
1144
|
</button>
|
|
767
1145
|
|
|
768
1146
|
<!-- Navigation -->
|
|
@@ -774,8 +1152,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
774
1152
|
(click)="prev(); $event.stopPropagation()"
|
|
775
1153
|
(mousedown)="$event.stopPropagation()"
|
|
776
1154
|
aria-label="Previous image"
|
|
1155
|
+
[innerHTML]="icons.prev"
|
|
777
1156
|
>
|
|
778
|
-
<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
|
|
779
1157
|
</button>
|
|
780
1158
|
}
|
|
781
1159
|
<!-- Check if not last -->
|
|
@@ -785,8 +1163,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
785
1163
|
(click)="next(); $event.stopPropagation()"
|
|
786
1164
|
(mousedown)="$event.stopPropagation()"
|
|
787
1165
|
aria-label="Next image"
|
|
1166
|
+
[innerHTML]="icons.next"
|
|
788
1167
|
>
|
|
789
|
-
<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
|
790
1168
|
</button>
|
|
791
1169
|
}
|
|
792
1170
|
|
|
@@ -795,36 +1173,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
795
1173
|
}
|
|
796
1174
|
|
|
797
1175
|
<!-- Toolbar -->
|
|
1176
|
+
<!-- Toolbar -->
|
|
1177
|
+
@if (showToolbar()) {
|
|
798
1178
|
<div
|
|
799
1179
|
class="toolbar"
|
|
800
1180
|
(click)="$event.stopPropagation()"
|
|
801
1181
|
(keydown)="onToolbarKey($event)"
|
|
802
1182
|
tabindex="0"
|
|
803
1183
|
>
|
|
804
|
-
<!-- Flip
|
|
805
|
-
|
|
806
|
-
<
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
<!--
|
|
813
|
-
|
|
814
|
-
<
|
|
815
|
-
|
|
816
|
-
<!-- Rotate
|
|
817
|
-
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1184
|
+
<!-- Flip -->
|
|
1185
|
+
@if (toolbarConfig().showFlip) {
|
|
1186
|
+
<button class="toolbar-btn" (click)="flipHorizontal()" aria-label="Flip Horizontal" [innerHTML]="icons.flipH">
|
|
1187
|
+
</button>
|
|
1188
|
+
<button class="toolbar-btn" (click)="flipVertical()" aria-label="Flip Vertical" [innerHTML]="icons.flipV">
|
|
1189
|
+
</button>
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
<!-- Extensions (Custom Buttons) -->
|
|
1193
|
+
@if (toolbarExtensions()) {
|
|
1194
|
+
<ng-container *ngTemplateOutlet="toolbarExtensions(); context: { $implicit: images()![currentIndex()] }"></ng-container>
|
|
1195
|
+
}
|
|
1196
|
+
<!-- Rotate -->
|
|
1197
|
+
@if (toolbarConfig().showRotate) {
|
|
1198
|
+
<button class="toolbar-btn" (click)="rotateLeft()" aria-label="Rotate Left" [innerHTML]="icons.rotateLeft">
|
|
1199
|
+
</button>
|
|
1200
|
+
<button class="toolbar-btn" (click)="rotateRight()" aria-label="Rotate Right" [innerHTML]="icons.rotateRight">
|
|
1201
|
+
</button>
|
|
1202
|
+
}
|
|
1203
|
+
<!-- Zoom -->
|
|
1204
|
+
@if (toolbarConfig().showZoom) {
|
|
1205
|
+
<button class="toolbar-btn" (click)="zoomOut()" aria-label="Zoom Out" [innerHTML]="icons.zoomOut">
|
|
1206
|
+
</button>
|
|
1207
|
+
<button class="toolbar-btn" (click)="zoomIn()" aria-label="Zoom In" [innerHTML]="icons.zoomIn">
|
|
1208
|
+
</button>
|
|
1209
|
+
}
|
|
1210
|
+
</div>
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
<!-- Thumbnails -->
|
|
1215
|
+
@if (images() && images()!.length > 1 && showThumbnails()) {
|
|
1216
|
+
<div class="thumbnail-strip" (click)="$event.stopPropagation()">
|
|
1217
|
+
@for (img of images(); track img; let i = $index) {
|
|
1218
|
+
<div
|
|
1219
|
+
#thumbRef
|
|
1220
|
+
class="thumbnail-item"
|
|
1221
|
+
[class.active]="i === currentIndex()"
|
|
1222
|
+
(click)="jumpTo(i)"
|
|
1223
|
+
>
|
|
1224
|
+
<img [src]="img" loading="lazy" alt="Thumbnail">
|
|
1225
|
+
</div>
|
|
1226
|
+
}
|
|
828
1227
|
</div>
|
|
829
1228
|
}
|
|
830
1229
|
</div>
|
|
@@ -843,8 +1242,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
843
1242
|
'(document:keydown.arrowleft)': 'prev()',
|
|
844
1243
|
'(document:keydown.arrowright)': 'next()',
|
|
845
1244
|
'(document:keydown.escape)': 'close()',
|
|
846
|
-
}, styles: [":host{display:block}.overlay{position:fixed;inset:0;z-index:
|
|
847
|
-
}], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: true }] }], images: [{ type: i0.Input, args: [{ isSignal: true, alias: "images", required: false }] }], initialIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialIndex", required: false }] }], customTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "customTemplate", required: false }] }],
|
|
1245
|
+
}, styles: [":host{display:block;--ng-img-background: rgba(0, 0, 0, .95);--ng-img-text-color: rgba(255, 255, 255, .8);--ng-img-z-index: 50;--ng-img-toolbar-bg: rgba(255, 255, 255, .1);--ng-img-toolbar-hover: rgba(255, 255, 255, .2);--ng-img-gap: 16px;--ng-img-item-bg: rgba(0, 0, 0, .3);--ng-img-thumb-strip-bg: rgba(0, 0, 0, .4);--ng-img-thumb-width: 60px;--ng-img-thumb-height: 40px;--ng-img-thumb-gap: 8px;--ng-img-thumb-border-radius: 6px;--ng-img-thumb-active-border: white}.overlay{position:fixed;inset:0;z-index:var(--ng-img-z-index);display:flex;align-items:center;justify-content:center;background-color:var(--ng-img-background);overflow:hidden;-webkit-user-select:none;user-select:none;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;touch-action:none}.loader,.error{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--ng-img-text-color);font-size:16px}.image-container{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;box-sizing:border-box;display:flex;align-items:center;justify-content:center;touch-action:none}.preview-image{position:absolute;top:0;left:0;width:100%;height:100%;margin:auto;object-fit:contain;pointer-events:auto;will-change:transform;transform-origin:center center;transition:transform .3s cubic-bezier(.2,0,.2,1);touch-action:none}.preview-image.dragging,.preview-image.inertia,.preview-image.pinching{cursor:move;transition:none}.preview-image.zoom-in{cursor:zoom-in}.preview-image.zoom-out{cursor:grab}.toolbar{position:absolute;bottom:30px;left:50%;transform:translate(-50%);display:flex;gap:var(--ng-img-gap);background-color:var(--ng-img-toolbar-bg);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:10px 20px;border-radius:24px;pointer-events:auto;z-index:calc(var(--ng-img-z-index) + 10)}.toolbar-btn{background:none;border:none;color:#fff;cursor:pointer;padding:8px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.toolbar-btn:hover{background-color:var(--ng-img-toolbar-hover)}.toolbar-btn:focus-visible{outline:2px solid white;outline-offset:2px}.toolbar-btn ::ng-deep svg{width:24px;height:24px;fill:currentColor}.close-btn{position:absolute;top:20px;right:20px;z-index:calc(var(--ng-img-z-index) + 10);background:none;border:none;color:#fff;cursor:pointer;padding:8px;border-radius:50%;background-color:#00000080;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.close-btn ::ng-deep svg{width:24px;height:24px;fill:currentColor}.close-btn:hover{background:var(--ng-img-toolbar-hover);transform:rotate(90deg)}.nav-btn{position:absolute;top:50%;transform:translateY(-50%);width:48px;height:48px;border-radius:50%;background:var(--ng-img-toolbar-bg);border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;z-index:10}.nav-btn:hover{background:var(--ng-img-toolbar-hover)}.nav-btn.prev{left:20px}.nav-btn.next{right:20px}.nav-btn ::ng-deep svg{width:32px;height:32px;fill:currentColor;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.counter{position:absolute;top:20px;left:50%;transform:translate(-50%);color:var(--ng-img-text-color);font-size:14px;background:var(--ng-img-item-bg);padding:4px 12px;border-radius:12px;pointer-events:none;z-index:10}.hidden{display:none}.thumbnail-strip{position:absolute;bottom:100px;left:50%;transform:translate(-50%);display:inline-block;white-space:nowrap;max-width:90%;width:auto;overflow-x:auto;overflow-y:hidden;padding:8px;background:var(--ng-img-thumb-strip-bg);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:12px;scroll-behavior:smooth;scrollbar-width:none;z-index:55;font-size:0}.thumbnail-strip::-webkit-scrollbar{display:none}.thumbnail-item{display:inline-block;vertical-align:middle;width:var(--ng-img-thumb-width);height:var(--ng-img-thumb-height);margin-right:var(--ng-img-thumb-gap);border-radius:var(--ng-img-thumb-border-radius);overflow:hidden;cursor:pointer;opacity:.6;transition:all .2s;border:2px solid transparent;box-sizing:border-box}.thumbnail-item:last-child{margin-right:0}.thumbnail-item:hover{opacity:.8}.thumbnail-item.active{opacity:1;border-color:var(--ng-img-thumb-active-border);transform:scale(1.1)}.thumbnail-item img{width:100%;height:100%;object-fit:cover}\n"] }]
|
|
1246
|
+
}], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: true }] }], images: [{ type: i0.Input, args: [{ isSignal: true, alias: "images", required: false }] }], srcset: [{ type: i0.Input, args: [{ isSignal: true, alias: "srcset", required: false }] }], srcsets: [{ type: i0.Input, args: [{ isSignal: true, alias: "srcsets", required: false }] }], initialIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialIndex", required: false }] }], openerRect: [{ type: i0.Input, args: [{ isSignal: true, alias: "openerRect", required: false }] }], customTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "customTemplate", required: false }] }], toolbarConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbarConfig", required: false }] }], imgRefs: [{ type: i0.ViewChildren, args: ['imgRef', { isSignal: true }] }], thumbRefs: [{ type: i0.ViewChildren, args: ['thumbRef', { isSignal: true }] }], showThumbnails: [{ type: i0.Input, args: [{ isSignal: true, alias: "showThumbnails", required: false }] }], showToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToolbar", required: false }] }], toolbarExtensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbarExtensions", required: false }] }], onEscape: [{
|
|
848
1247
|
type: HostListener,
|
|
849
1248
|
args: ['document:keydown.escape']
|
|
850
1249
|
}], onMouseMove: [{
|
|
@@ -852,10 +1251,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
852
1251
|
args: ['document:mousemove', ['$event']]
|
|
853
1252
|
}] } });
|
|
854
1253
|
|
|
1254
|
+
/**
|
|
1255
|
+
* Directive to open the image preview.
|
|
1256
|
+
* Can be used on any element, but auto-detects `src` on `img` or nested `img`.
|
|
1257
|
+
*/
|
|
855
1258
|
class ImagesPreviewDirective {
|
|
1259
|
+
/**
|
|
1260
|
+
* High resolution image source to display in preview.
|
|
1261
|
+
* If empty, tries to read `src` from host element or first child `img`.
|
|
1262
|
+
*/
|
|
856
1263
|
highResSrc = '';
|
|
1264
|
+
/**
|
|
1265
|
+
* List of images for gallery navigation.
|
|
1266
|
+
* If provided, enables previous/next navigation buttons.
|
|
1267
|
+
*/
|
|
857
1268
|
previewImages = [];
|
|
1269
|
+
/**
|
|
1270
|
+
* Custom template to use for the preview.
|
|
1271
|
+
* If provided, overrides the default viewer UI.
|
|
1272
|
+
*/
|
|
858
1273
|
previewTemplate;
|
|
1274
|
+
/**
|
|
1275
|
+
* Configuration for the toolbar buttons.
|
|
1276
|
+
*/
|
|
1277
|
+
toolbarConfig;
|
|
1278
|
+
/**
|
|
1279
|
+
* Optional srcset for the single image.
|
|
1280
|
+
*/
|
|
1281
|
+
srcset;
|
|
1282
|
+
/**
|
|
1283
|
+
* List of srcsets corresponding to the `previewImages` array.
|
|
1284
|
+
*/
|
|
1285
|
+
/**
|
|
1286
|
+
* List of srcsets corresponding to the `previewImages` array.
|
|
1287
|
+
*/
|
|
1288
|
+
srcsets;
|
|
1289
|
+
/**
|
|
1290
|
+
* Whether to show the thumbnail strip.
|
|
1291
|
+
* @default true
|
|
1292
|
+
*/
|
|
1293
|
+
showThumbnails = true;
|
|
1294
|
+
/**
|
|
1295
|
+
* Whether to show the toolbar.
|
|
1296
|
+
* @default true
|
|
1297
|
+
*/
|
|
1298
|
+
showToolbar = true;
|
|
1299
|
+
/**
|
|
1300
|
+
* Custom template to render in the toolbar (e.g. for download buttons).
|
|
1301
|
+
*/
|
|
1302
|
+
toolbarExtensions = null;
|
|
1303
|
+
/**
|
|
1304
|
+
* Type guard helper for strict template type checking.
|
|
1305
|
+
* Allows Angular Language Service to infer types in `previewTemplate`.
|
|
1306
|
+
*/
|
|
1307
|
+
static ngTemplateContextGuard(directive, context) {
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
859
1310
|
componentRef = null;
|
|
860
1311
|
appRef = inject(ApplicationRef);
|
|
861
1312
|
injector = inject(EnvironmentInjector);
|
|
@@ -878,11 +1329,12 @@ class ImagesPreviewDirective {
|
|
|
878
1329
|
}
|
|
879
1330
|
src = src || '';
|
|
880
1331
|
if (src) {
|
|
881
|
-
|
|
1332
|
+
const rect = hostEl.getBoundingClientRect();
|
|
1333
|
+
this.openPreview(src, rect);
|
|
882
1334
|
}
|
|
883
1335
|
}
|
|
884
1336
|
cursor = 'pointer';
|
|
885
|
-
openPreview(src) {
|
|
1337
|
+
openPreview(src, rect) {
|
|
886
1338
|
if (!isPlatformBrowser(this.platformId))
|
|
887
1339
|
return;
|
|
888
1340
|
// Create Component
|
|
@@ -891,6 +1343,7 @@ class ImagesPreviewDirective {
|
|
|
891
1343
|
});
|
|
892
1344
|
// Set Inputs
|
|
893
1345
|
this.componentRef.setInput('src', src);
|
|
1346
|
+
this.componentRef.setInput('openerRect', rect);
|
|
894
1347
|
if (this.previewImages.length > 0) {
|
|
895
1348
|
this.componentRef.setInput('images', this.previewImages);
|
|
896
1349
|
const index = this.previewImages.indexOf(src);
|
|
@@ -899,6 +1352,18 @@ class ImagesPreviewDirective {
|
|
|
899
1352
|
if (this.previewTemplate) {
|
|
900
1353
|
this.componentRef.setInput('customTemplate', this.previewTemplate);
|
|
901
1354
|
}
|
|
1355
|
+
if (this.toolbarConfig) {
|
|
1356
|
+
this.componentRef.setInput('toolbarConfig', this.toolbarConfig);
|
|
1357
|
+
}
|
|
1358
|
+
if (this.srcset) {
|
|
1359
|
+
this.componentRef.setInput('srcset', this.srcset);
|
|
1360
|
+
}
|
|
1361
|
+
if (this.srcsets) {
|
|
1362
|
+
this.componentRef.setInput('srcsets', this.srcsets);
|
|
1363
|
+
}
|
|
1364
|
+
this.componentRef.setInput('showThumbnails', this.showThumbnails);
|
|
1365
|
+
this.componentRef.setInput('showToolbar', this.showToolbar);
|
|
1366
|
+
this.componentRef.setInput('toolbarExtensions', this.toolbarExtensions);
|
|
902
1367
|
// Set Callbacks
|
|
903
1368
|
this.componentRef.instance.closeCallback = () => this.destroyPreview();
|
|
904
1369
|
// Attach to App
|
|
@@ -918,7 +1383,7 @@ class ImagesPreviewDirective {
|
|
|
918
1383
|
this.destroyPreview();
|
|
919
1384
|
}
|
|
920
1385
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: ImagesPreviewDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
921
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.7", type: ImagesPreviewDirective, isStandalone: true, selector: "[ngImagesPreview]", inputs: { highResSrc: ["ngImagesPreview", "highResSrc"], previewImages: "previewImages", previewTemplate: "previewTemplate" }, host: { listeners: { "click": "onClick($event)", "style.cursor": "cursor()" } }, ngImport: i0 });
|
|
1386
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.7", type: ImagesPreviewDirective, isStandalone: true, selector: "[ngImagesPreview]", inputs: { highResSrc: ["ngImagesPreview", "highResSrc"], previewImages: "previewImages", previewTemplate: "previewTemplate", toolbarConfig: "toolbarConfig", srcset: "srcset", srcsets: "srcsets", showThumbnails: "showThumbnails", showToolbar: "showToolbar", toolbarExtensions: "toolbarExtensions" }, host: { listeners: { "click": "onClick($event)", "style.cursor": "cursor()" } }, ngImport: i0 });
|
|
922
1387
|
}
|
|
923
1388
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: ImagesPreviewDirective, decorators: [{
|
|
924
1389
|
type: Directive,
|
|
@@ -933,6 +1398,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
933
1398
|
type: Input
|
|
934
1399
|
}], previewTemplate: [{
|
|
935
1400
|
type: Input
|
|
1401
|
+
}], toolbarConfig: [{
|
|
1402
|
+
type: Input
|
|
1403
|
+
}], srcset: [{
|
|
1404
|
+
type: Input
|
|
1405
|
+
}], srcsets: [{
|
|
1406
|
+
type: Input
|
|
1407
|
+
}], showThumbnails: [{
|
|
1408
|
+
type: Input
|
|
1409
|
+
}], showToolbar: [{
|
|
1410
|
+
type: Input
|
|
1411
|
+
}], toolbarExtensions: [{
|
|
1412
|
+
type: Input
|
|
936
1413
|
}], onClick: [{
|
|
937
1414
|
type: HostListener,
|
|
938
1415
|
args: ['click', ['$event']]
|
|
@@ -941,6 +1418,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
941
1418
|
args: ['style.cursor']
|
|
942
1419
|
}] } });
|
|
943
1420
|
|
|
1421
|
+
class NgImagesPreviewModule {
|
|
1422
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: NgImagesPreviewModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
1423
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.7", ngImport: i0, type: NgImagesPreviewModule, imports: [ImagesPreviewComponent, ImagesPreviewDirective], exports: [ImagesPreviewComponent, ImagesPreviewDirective] });
|
|
1424
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: NgImagesPreviewModule, imports: [ImagesPreviewComponent] });
|
|
1425
|
+
}
|
|
1426
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: NgImagesPreviewModule, decorators: [{
|
|
1427
|
+
type: NgModule,
|
|
1428
|
+
args: [{
|
|
1429
|
+
imports: [ImagesPreviewComponent, ImagesPreviewDirective],
|
|
1430
|
+
exports: [ImagesPreviewComponent, ImagesPreviewDirective]
|
|
1431
|
+
}]
|
|
1432
|
+
}] });
|
|
1433
|
+
|
|
944
1434
|
/*
|
|
945
1435
|
* Public API Surface of ng-images-preview
|
|
946
1436
|
*/
|
|
@@ -949,5 +1439,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
949
1439
|
* Generated bundle index. Do not edit.
|
|
950
1440
|
*/
|
|
951
1441
|
|
|
952
|
-
export { ImagesPreviewComponent, ImagesPreviewDirective };
|
|
1442
|
+
export { ImagesPreviewComponent, ImagesPreviewDirective, NgImagesPreviewModule };
|
|
953
1443
|
//# sourceMappingURL=ng-images-preview.mjs.map
|