ng-images-preview 1.0.3 → 2.0.1
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 +81 -7
- package/fesm2022/ng-images-preview.mjs +687 -192
- package/fesm2022/ng-images-preview.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ng-images-preview.d.ts +168 -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,8 +214,10 @@ 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(),
|
|
219
|
+
currentIndex: this.currentIndex(),
|
|
220
|
+
total: this.images()?.length ?? 1,
|
|
79
221
|
}), ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
80
222
|
// Actions object for template
|
|
81
223
|
actions = {
|
|
@@ -87,6 +229,9 @@ class ImagesPreviewComponent {
|
|
|
87
229
|
flipVertical: () => this.flipVertical(),
|
|
88
230
|
reset: () => this.reset(),
|
|
89
231
|
close: () => this.close(),
|
|
232
|
+
next: () => this.next(),
|
|
233
|
+
prev: () => this.prev(),
|
|
234
|
+
jumpTo: (index) => this.jumpTo(index),
|
|
90
235
|
};
|
|
91
236
|
MIN_SCALE = 0.5;
|
|
92
237
|
MAX_SCALE = 5;
|
|
@@ -102,18 +247,92 @@ class ImagesPreviewComponent {
|
|
|
102
247
|
cachedConstraints = null;
|
|
103
248
|
lastTimestamp = 0;
|
|
104
249
|
rafId = null;
|
|
105
|
-
FRICTION = 0.
|
|
250
|
+
FRICTION = 0.95; // Super smooth glide
|
|
106
251
|
VELOCITY_THRESHOLD = 0.01;
|
|
107
252
|
MAX_VELOCITY = 3; // Cap speed to prevent teleporting
|
|
108
|
-
|
|
253
|
+
// Updated transform logic for slide buffer
|
|
254
|
+
getTransform(offset) {
|
|
255
|
+
const viewportWidth = window.innerWidth;
|
|
256
|
+
const spacing = 16; // Gap between images
|
|
257
|
+
const baseOffset = offset * (viewportWidth + spacing);
|
|
258
|
+
// Dynamic movement from touch/inertia
|
|
259
|
+
// Global translateX applies to the whole "track"
|
|
260
|
+
const x = this.translateX() + baseOffset;
|
|
261
|
+
// Y-axis drag (Pull-to-Close) usually only affects the active image visually,
|
|
262
|
+
// but moving the whole track is acceptable and simpler.
|
|
263
|
+
// Ideally, neighbors stay at Y=0.
|
|
264
|
+
const effectiveY = offset === 0 ? this.translateY() : 0;
|
|
265
|
+
const effectiveScale = offset === 0 ? this.scale() : 1;
|
|
266
|
+
const effectiveRotate = offset === 0 ? this.rotate() : 0;
|
|
267
|
+
const flipH = offset === 0 ? this.flipH() : false;
|
|
268
|
+
const flipV = offset === 0 ? this.flipV() : false;
|
|
269
|
+
const scaleX = flipH ? -1 : 1;
|
|
270
|
+
const scaleY = flipV ? -1 : 1;
|
|
271
|
+
// FLIP override
|
|
272
|
+
if (offset === 0) {
|
|
273
|
+
const flip = this.flipTransform();
|
|
274
|
+
if (flip)
|
|
275
|
+
return flip;
|
|
276
|
+
}
|
|
277
|
+
return `translate3d(${x}px, ${effectiveY}px, 0) scale(${effectiveScale}) rotate(${effectiveRotate}deg) scaleX(${scaleX}) scaleY(${scaleY})`;
|
|
278
|
+
}
|
|
279
|
+
// FLIP State
|
|
280
|
+
flipTransform = signal('', ...(ngDevMode ? [{ debugName: "flipTransform" }] : []));
|
|
281
|
+
// Helper to get current active image element
|
|
282
|
+
getCurrentImageElement() {
|
|
283
|
+
const buffer = this.activeBuffer();
|
|
284
|
+
const index = buffer.findIndex((item) => item.offset === 0);
|
|
285
|
+
if (index === -1)
|
|
286
|
+
return undefined;
|
|
287
|
+
return this.imgRefs()[index]?.nativeElement;
|
|
288
|
+
}
|
|
289
|
+
runFlipAnimation(opener) {
|
|
290
|
+
if (!isPlatformBrowser(this.platformId))
|
|
291
|
+
return;
|
|
292
|
+
const imgEl = this.getCurrentImageElement();
|
|
293
|
+
if (!imgEl)
|
|
294
|
+
return;
|
|
295
|
+
// 1. First: Final state is already rendered (centered, max fit)
|
|
296
|
+
const finalRect = imgEl.getBoundingClientRect();
|
|
297
|
+
// 2. Invert: Calculate scale and translate to match opener
|
|
298
|
+
const scaleX = opener.width / finalRect.width;
|
|
299
|
+
const scaleY = opener.height / finalRect.height;
|
|
300
|
+
// Note: we use the larger scale to crop?
|
|
301
|
+
// Or fit?
|
|
302
|
+
// Usually we want to fit.
|
|
303
|
+
// Let's matching fitting.
|
|
304
|
+
// Actually, most simple FLIP matches dimensions exactly.
|
|
305
|
+
// But aspect ratios might differ.
|
|
306
|
+
// Let's just match the rect exactly.
|
|
307
|
+
const deltaX = opener.left - finalRect.left + (opener.width - finalRect.width) / 2;
|
|
308
|
+
const deltaY = opener.top - finalRect.top + (opener.height - finalRect.height) / 2;
|
|
309
|
+
// Apply Invert Transform immediately (no transition)
|
|
310
|
+
imgEl.style.transition = 'none';
|
|
311
|
+
this.flipTransform.set(`translate3d(${deltaX}px, ${deltaY}px, 0) scale(${scaleX}, ${scaleY})`);
|
|
312
|
+
// Force Reflow
|
|
313
|
+
imgEl.offsetHeight;
|
|
314
|
+
// 3. Play
|
|
315
|
+
requestAnimationFrame(() => {
|
|
316
|
+
// Enable transition
|
|
317
|
+
imgEl.style.transition = 'transform 300ms cubic-bezier(0.2, 0, 0.2, 1)';
|
|
318
|
+
// Remove override (animates to Final)
|
|
319
|
+
this.flipTransform.set('');
|
|
320
|
+
this.flipAnimDone = true;
|
|
321
|
+
// CLEANUP: Remove transition after animation wraps up so dragging/panning is instant
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
imgEl.style.transition = '';
|
|
324
|
+
}, 300);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
overlayBackground = computed(() => {
|
|
328
|
+
const y = Math.abs(this.translateY());
|
|
109
329
|
const scale = this.scale();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}, ...(ngDevMode ? [{ debugName: "transformStyle" }] : []));
|
|
330
|
+
if (scale === 1 && y > 0) {
|
|
331
|
+
const opacity = Math.max(0, 0.95 - y / 400); // Fade out as we drag down
|
|
332
|
+
return `rgba(0, 0, 0, ${opacity})`;
|
|
333
|
+
}
|
|
334
|
+
return 'var(--ng-img-background)';
|
|
335
|
+
}, ...(ngDevMode ? [{ debugName: "overlayBackground" }] : []));
|
|
117
336
|
onEscape() {
|
|
118
337
|
this.close();
|
|
119
338
|
}
|
|
@@ -134,13 +353,19 @@ class ImagesPreviewComponent {
|
|
|
134
353
|
this.translateY.set(nextY);
|
|
135
354
|
return;
|
|
136
355
|
}
|
|
137
|
-
const img = this.
|
|
356
|
+
const img = this.getCurrentImageElement();
|
|
138
357
|
if (!img) {
|
|
139
358
|
this.translateX.set(nextX);
|
|
140
359
|
this.translateY.set(nextY);
|
|
141
360
|
return;
|
|
142
361
|
}
|
|
143
362
|
const scale = this.scale();
|
|
363
|
+
// If scale is 1, we allow free movement for Pull-to-Close and Swipe Nav
|
|
364
|
+
if (scale === 1) {
|
|
365
|
+
this.translateX.set(nextX);
|
|
366
|
+
this.translateY.set(nextY);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
144
369
|
const rotate = Math.abs(this.rotate() % 180);
|
|
145
370
|
const isRotated = rotate === 90;
|
|
146
371
|
const baseWidth = img.offsetWidth;
|
|
@@ -158,7 +383,7 @@ class ImagesPreviewComponent {
|
|
|
158
383
|
this.translateY.set(clampedY);
|
|
159
384
|
}
|
|
160
385
|
onTouchMove(event) {
|
|
161
|
-
if (!this.isDragging() && !this.isPinching)
|
|
386
|
+
if (!this.isDragging() && !this.isPinching())
|
|
162
387
|
return;
|
|
163
388
|
// Prevent default to stop page scrolling/zooming
|
|
164
389
|
if (event.cancelable) {
|
|
@@ -166,7 +391,7 @@ class ImagesPreviewComponent {
|
|
|
166
391
|
}
|
|
167
392
|
const touches = event.touches;
|
|
168
393
|
// One finger: Pan
|
|
169
|
-
if (touches.length === 1 && this.isDragging() && !this.isPinching) {
|
|
394
|
+
if (touches.length === 1 && this.isDragging() && !this.isPinching()) {
|
|
170
395
|
const now = Date.now();
|
|
171
396
|
// Add point to history
|
|
172
397
|
this.touchHistory.push({
|
|
@@ -190,11 +415,30 @@ class ImagesPreviewComponent {
|
|
|
190
415
|
// Apply rubber banding if out of bounds
|
|
191
416
|
// Use cached constraints to avoid reflows
|
|
192
417
|
const constraints = this.cachedConstraints || this.getConstraints();
|
|
193
|
-
|
|
194
|
-
|
|
418
|
+
// At scale 1, we want linear drag for pull-to-close (Y) and swipe (X)
|
|
419
|
+
// But we can keep rubber banding if it feels good.
|
|
420
|
+
// For Pull-to-Close, standard is 1:1 or slightly resisted.
|
|
421
|
+
// Let's stick to the current logic which applies resistance if "out of bounds".
|
|
422
|
+
// Since at scale 1 bounds are 0, it will apply resistance immediately.
|
|
423
|
+
// We want easier pull.
|
|
424
|
+
if (this.scale() === 1) {
|
|
425
|
+
// Reduce resistance
|
|
426
|
+
nextY = currentY + deltaY;
|
|
427
|
+
nextX = currentX + deltaX;
|
|
195
428
|
}
|
|
196
|
-
else
|
|
197
|
-
|
|
429
|
+
else {
|
|
430
|
+
if (nextX > constraints.maxX) {
|
|
431
|
+
nextX = constraints.maxX + (nextX - constraints.maxX) * 0.5;
|
|
432
|
+
}
|
|
433
|
+
else if (nextX < -constraints.maxX) {
|
|
434
|
+
nextX = -constraints.maxX + (nextX + constraints.maxX) * 0.5;
|
|
435
|
+
}
|
|
436
|
+
if (nextY > constraints.maxY) {
|
|
437
|
+
nextY = constraints.maxY + (nextY - constraints.maxY) * 0.5;
|
|
438
|
+
}
|
|
439
|
+
else if (nextY < -constraints.maxY) {
|
|
440
|
+
nextY = -constraints.maxY + (nextY + constraints.maxY) * 0.5;
|
|
441
|
+
}
|
|
198
442
|
}
|
|
199
443
|
if (nextY > constraints.maxY) {
|
|
200
444
|
nextY = constraints.maxY + (nextY - constraints.maxY) * 0.5;
|
|
@@ -208,15 +452,44 @@ class ImagesPreviewComponent {
|
|
|
208
452
|
this.lastTouchY = touches[0].clientY;
|
|
209
453
|
}
|
|
210
454
|
// Two fingers: Pinch Zoom
|
|
211
|
-
if (touches.length === 2) {
|
|
455
|
+
if (touches.length === 2 && this.initialPinchDistance > 0) {
|
|
212
456
|
const distance = this.getDistance(touches);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
457
|
+
const scaleFactor = distance / this.initialPinchDistance;
|
|
458
|
+
// Calculate new scale with limits
|
|
459
|
+
const rawNewScale = this.initialScale * scaleFactor;
|
|
460
|
+
const newScale = Math.min(Math.max(rawNewScale, this.MIN_SCALE), this.MAX_SCALE);
|
|
461
|
+
// Calculate new translation to keep focal point fixed
|
|
462
|
+
const effectiveRatio = newScale / this.initialScale;
|
|
463
|
+
const currentCenter = this.getCenter(touches);
|
|
464
|
+
const cx = currentCenter.x - window.innerWidth / 2;
|
|
465
|
+
const cy = currentCenter.y - window.innerHeight / 2;
|
|
466
|
+
const sx = this.initialPinchCenter.x - window.innerWidth / 2;
|
|
467
|
+
const sy = this.initialPinchCenter.y - window.innerHeight / 2;
|
|
468
|
+
let newTx = cx - (sx - this.initialTranslateX) * effectiveRatio;
|
|
469
|
+
let newTy = cy - (sy - this.initialTranslateY) * effectiveRatio;
|
|
470
|
+
// --- Clamp Logic (Inline for atomicity) ---
|
|
471
|
+
const img = this.getCurrentImageElement();
|
|
472
|
+
if (img) {
|
|
473
|
+
const rotate = Math.abs(this.rotate() % 180);
|
|
474
|
+
const isRotated = rotate === 90;
|
|
475
|
+
const baseWidth = img.offsetWidth;
|
|
476
|
+
const baseHeight = img.offsetHeight;
|
|
477
|
+
const currentWidth = (isRotated ? baseHeight : baseWidth) * newScale;
|
|
478
|
+
const currentHeight = (isRotated ? baseWidth : baseHeight) * newScale;
|
|
479
|
+
const maxTx = Math.max(0, (currentWidth - window.innerWidth) / 2);
|
|
480
|
+
const maxTy = Math.max(0, (currentHeight - window.innerHeight) / 2);
|
|
481
|
+
if (currentWidth <= window.innerWidth)
|
|
482
|
+
newTx = 0;
|
|
483
|
+
else if (Math.abs(newTx) > maxTx)
|
|
484
|
+
newTx = Math.sign(newTx) * maxTx;
|
|
485
|
+
if (currentHeight <= window.innerHeight)
|
|
486
|
+
newTy = 0;
|
|
487
|
+
else if (Math.abs(newTy) > maxTy)
|
|
488
|
+
newTy = Math.sign(newTy) * maxTy;
|
|
219
489
|
}
|
|
490
|
+
this.scale.set(newScale);
|
|
491
|
+
this.translateX.set(newTx);
|
|
492
|
+
this.translateY.set(newTy);
|
|
220
493
|
}
|
|
221
494
|
}
|
|
222
495
|
getConstraints() {
|
|
@@ -226,7 +499,7 @@ class ImagesPreviewComponent {
|
|
|
226
499
|
// The `cachedConstraints` property is used *outside* this method.
|
|
227
500
|
if (!isPlatformBrowser(this.platformId))
|
|
228
501
|
return { maxX: 0, maxY: 0 };
|
|
229
|
-
const img = this.
|
|
502
|
+
const img = this.getCurrentImageElement();
|
|
230
503
|
if (!img)
|
|
231
504
|
return { maxX: 0, maxY: 0 };
|
|
232
505
|
const scale = this.scale();
|
|
@@ -246,7 +519,7 @@ class ImagesPreviewComponent {
|
|
|
246
519
|
clampPosition() {
|
|
247
520
|
if (!isPlatformBrowser(this.platformId))
|
|
248
521
|
return;
|
|
249
|
-
const img = this.
|
|
522
|
+
const img = this.getCurrentImageElement();
|
|
250
523
|
if (!img)
|
|
251
524
|
return;
|
|
252
525
|
const scale = this.scale();
|
|
@@ -283,6 +556,7 @@ class ImagesPreviewComponent {
|
|
|
283
556
|
this.translateY.set(newY);
|
|
284
557
|
}
|
|
285
558
|
startInertia() {
|
|
559
|
+
this.isInertia.set(true); // Disable CSS transition
|
|
286
560
|
let lastTime = Date.now();
|
|
287
561
|
const step = () => {
|
|
288
562
|
if (!this.isDragging() &&
|
|
@@ -292,7 +566,6 @@ class ImagesPreviewComponent {
|
|
|
292
566
|
const dt = Math.min(now - lastTime, 64); // Cap dt to avoid huge jumps if lag
|
|
293
567
|
lastTime = now;
|
|
294
568
|
if (dt === 0) {
|
|
295
|
-
// Skip if 0ms passed (can happen on fast screens)
|
|
296
569
|
this.rafId = requestAnimationFrame(step);
|
|
297
570
|
return;
|
|
298
571
|
}
|
|
@@ -300,14 +573,12 @@ class ImagesPreviewComponent {
|
|
|
300
573
|
this.velocityX = Math.max(-this.MAX_VELOCITY, Math.min(this.MAX_VELOCITY, this.velocityX));
|
|
301
574
|
this.velocityY = Math.max(-this.MAX_VELOCITY, Math.min(this.MAX_VELOCITY, this.velocityY));
|
|
302
575
|
// Time-based friction
|
|
303
|
-
// Standard friction is 0.95 per 16ms frame
|
|
304
576
|
const frictionFactor = Math.pow(this.FRICTION, dt / 16);
|
|
305
577
|
this.velocityX *= frictionFactor;
|
|
306
578
|
this.velocityY *= frictionFactor;
|
|
307
579
|
let nextX = this.translateX() + this.velocityX * dt;
|
|
308
580
|
let nextY = this.translateY() + this.velocityY * dt;
|
|
309
581
|
// Check bounds during inertia
|
|
310
|
-
// Hard stop/bounce logic can be improved here if needed
|
|
311
582
|
const constraints = this.cachedConstraints || this.getConstraints();
|
|
312
583
|
if (nextX > constraints.maxX) {
|
|
313
584
|
nextX = constraints.maxX;
|
|
@@ -331,14 +602,12 @@ class ImagesPreviewComponent {
|
|
|
331
602
|
}
|
|
332
603
|
else {
|
|
333
604
|
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
605
|
}
|
|
338
606
|
};
|
|
339
607
|
this.rafId = requestAnimationFrame(step);
|
|
340
608
|
}
|
|
341
609
|
stopInertia() {
|
|
610
|
+
this.isInertia.set(false); // Re-enable CSS transition
|
|
342
611
|
if (this.rafId) {
|
|
343
612
|
cancelAnimationFrame(this.rafId);
|
|
344
613
|
this.rafId = null;
|
|
@@ -380,6 +649,27 @@ class ImagesPreviewComponent {
|
|
|
380
649
|
event.stopPropagation();
|
|
381
650
|
}
|
|
382
651
|
onMouseUp() {
|
|
652
|
+
if (this.isDragging()) {
|
|
653
|
+
// Check Pull to Close for Mouse
|
|
654
|
+
if (this.scale() === 1) {
|
|
655
|
+
const y = this.translateY();
|
|
656
|
+
if (Math.abs(y) > 100) {
|
|
657
|
+
this.close();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
// Reset position if not closed
|
|
661
|
+
if (y !== 0) {
|
|
662
|
+
this.translateY.set(0);
|
|
663
|
+
}
|
|
664
|
+
const x = this.translateX();
|
|
665
|
+
if (x !== 0) {
|
|
666
|
+
this.translateX.set(0);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
this.snapBack();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
383
673
|
this.isDragging.set(false);
|
|
384
674
|
}
|
|
385
675
|
onTouchEnd(event) {
|
|
@@ -405,42 +695,55 @@ class ImagesPreviewComponent {
|
|
|
405
695
|
this.velocityY = 0;
|
|
406
696
|
}
|
|
407
697
|
this.isDragging.set(false);
|
|
408
|
-
this.isPinching
|
|
698
|
+
this.isPinching.set(false);
|
|
409
699
|
this.initialPinchDistance = 0;
|
|
410
700
|
// Swipe Navigation (at 1x scale)
|
|
411
701
|
if (this.scale() === 1) {
|
|
412
702
|
const x = this.translateX();
|
|
413
|
-
const
|
|
703
|
+
const y = this.translateY();
|
|
704
|
+
// Pull to Close
|
|
705
|
+
if (Math.abs(y) > 100) {
|
|
706
|
+
this.close();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const threshold = window.innerWidth * 0.25;
|
|
710
|
+
const images = this.images();
|
|
711
|
+
const total = images ? images.length : 0;
|
|
712
|
+
const index = this.currentIndex();
|
|
414
713
|
if (x < -threshold) {
|
|
415
|
-
|
|
714
|
+
// NEXT (Slide Left)
|
|
715
|
+
if (index < total - 1) {
|
|
716
|
+
this.animateSlide(-1);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
this.translateX.set(0);
|
|
720
|
+
this.translateY.set(0);
|
|
721
|
+
}
|
|
416
722
|
return;
|
|
417
723
|
}
|
|
418
724
|
else if (x > threshold) {
|
|
419
|
-
|
|
725
|
+
// PREV (Slide Right)
|
|
726
|
+
if (index > 0) {
|
|
727
|
+
this.animateSlide(1);
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
this.translateX.set(0);
|
|
731
|
+
this.translateY.set(0);
|
|
732
|
+
}
|
|
420
733
|
return;
|
|
421
734
|
}
|
|
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;
|
|
735
|
+
// Snap back
|
|
736
|
+
this.translateX.set(0);
|
|
737
|
+
this.translateY.set(0);
|
|
436
738
|
}
|
|
437
739
|
else {
|
|
438
740
|
this.startInertia();
|
|
439
741
|
}
|
|
742
|
+
return;
|
|
440
743
|
}
|
|
441
|
-
else if (touches.length === 1 && this.isPinching) {
|
|
744
|
+
else if (touches.length === 1 && this.isPinching()) {
|
|
442
745
|
// Transition from pinch to pan
|
|
443
|
-
this.isPinching
|
|
746
|
+
this.isPinching.set(false);
|
|
444
747
|
this.isDragging.set(true);
|
|
445
748
|
this.lastTouchX = touches[0].clientX;
|
|
446
749
|
this.lastTouchY = touches[0].clientY;
|
|
@@ -454,18 +757,32 @@ class ImagesPreviewComponent {
|
|
|
454
757
|
}
|
|
455
758
|
lastTouchX = 0;
|
|
456
759
|
lastTouchY = 0;
|
|
760
|
+
initialPinchCenter = { x: 0, y: 0 };
|
|
761
|
+
initialTranslateX = 0;
|
|
762
|
+
initialTranslateY = 0;
|
|
457
763
|
getDistance(touches) {
|
|
458
764
|
return Math.hypot(touches[0].clientX - touches[1].clientX, touches[0].clientY - touches[1].clientY);
|
|
459
765
|
}
|
|
766
|
+
getCenter(touches) {
|
|
767
|
+
return {
|
|
768
|
+
x: (touches[0].clientX + touches[1].clientX) / 2,
|
|
769
|
+
y: (touches[0].clientY + touches[1].clientY) / 2,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
460
772
|
close() {
|
|
461
773
|
this.closeCallback();
|
|
462
774
|
}
|
|
463
|
-
onImageLoad() {
|
|
464
|
-
this.
|
|
775
|
+
onImageLoad(src) {
|
|
776
|
+
this.loadedImages.update(set => {
|
|
777
|
+
const newSet = new Set(set);
|
|
778
|
+
newSet.add(src);
|
|
779
|
+
return newSet;
|
|
780
|
+
});
|
|
465
781
|
this.hasError.set(false);
|
|
466
782
|
}
|
|
467
|
-
onImageError() {
|
|
468
|
-
this.
|
|
783
|
+
onImageError(src) {
|
|
784
|
+
// this.loadedImages.update... (maybe not mark as loaded? or separate error tracking)
|
|
785
|
+
// For now, keep error simple
|
|
469
786
|
this.hasError.set(true);
|
|
470
787
|
}
|
|
471
788
|
// Zoom
|
|
@@ -521,14 +838,45 @@ class ImagesPreviewComponent {
|
|
|
521
838
|
this.currentIndex.update((i) => i - 1);
|
|
522
839
|
}
|
|
523
840
|
}
|
|
841
|
+
jumpTo(index) {
|
|
842
|
+
this.currentIndex.set(index);
|
|
843
|
+
this.reset();
|
|
844
|
+
}
|
|
845
|
+
// Slide Animation
|
|
846
|
+
animateSlide(direction) {
|
|
847
|
+
const spacing = 16;
|
|
848
|
+
const width = window.innerWidth + spacing;
|
|
849
|
+
// direction -1 (Next) -> move to -width
|
|
850
|
+
// direction 1 (Prev) -> move to width
|
|
851
|
+
// Wait. If I swipe Left (Next), x is negative. Target should be -width.
|
|
852
|
+
// If I swipe Right (Prev), x is positive. Target should be width.
|
|
853
|
+
// So target = direction * width? No.
|
|
854
|
+
// If direction is "Next" (index + 1), I want to slide to the Left (-X).
|
|
855
|
+
// Let's pass the sign of movement explicitly.
|
|
856
|
+
const target = direction === -1 ? -width : width;
|
|
857
|
+
this.translateX.set(target);
|
|
858
|
+
// Wait for CSS transition (0.3s)
|
|
859
|
+
setTimeout(() => {
|
|
860
|
+
// Update Index (silent swap)
|
|
861
|
+
this.isInertia.set(true); // Disable transition
|
|
862
|
+
if (direction === -1)
|
|
863
|
+
this.next();
|
|
864
|
+
else
|
|
865
|
+
this.prev();
|
|
866
|
+
this.translateX.set(0);
|
|
867
|
+
// Re-enable transition
|
|
868
|
+
setTimeout(() => {
|
|
869
|
+
this.isInertia.set(false);
|
|
870
|
+
}, 50);
|
|
871
|
+
}, 300);
|
|
872
|
+
}
|
|
524
873
|
// Mouse Interaction
|
|
525
874
|
onMouseDown(event) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
if (this.scale() <= 1)
|
|
875
|
+
// Allow dragging even at scale 1 for Pull-to-Close and Swipe-like nav (if intended)
|
|
876
|
+
if (this.isDragging())
|
|
877
|
+
return;
|
|
878
|
+
// Only trigger on Left Click
|
|
879
|
+
if (event.button !== 0)
|
|
532
880
|
return;
|
|
533
881
|
this.isDragging.set(true);
|
|
534
882
|
this.startX = event.clientX;
|
|
@@ -540,36 +888,38 @@ class ImagesPreviewComponent {
|
|
|
540
888
|
// Touch Interaction (bound in template)
|
|
541
889
|
onTouchStart(event) {
|
|
542
890
|
this.stopInertia(); // Cancel any ongoing movement
|
|
891
|
+
// if (event.cancelable) event.preventDefault(); // Removed to restore Tap-to-Close (click events)
|
|
892
|
+
// touch-action: none in CSS handles scroll prevention.
|
|
543
893
|
const touches = event.touches;
|
|
544
894
|
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
|
-
}
|
|
895
|
+
// Single touch: Pan.
|
|
896
|
+
// Allow dragging from anywhere in the container (better UX for Pull-to-Close)
|
|
897
|
+
this.isDragging.set(true);
|
|
898
|
+
this.lastTouchX = touches[0].clientX;
|
|
899
|
+
this.lastTouchY = touches[0].clientY;
|
|
900
|
+
this.lastTimestamp = Date.now();
|
|
901
|
+
// Initialize physics state
|
|
902
|
+
this.velocityX = 0;
|
|
903
|
+
this.velocityY = 0;
|
|
904
|
+
this.touchHistory = [
|
|
905
|
+
{
|
|
906
|
+
x: touches[0].clientX,
|
|
907
|
+
y: touches[0].clientY,
|
|
908
|
+
time: Date.now(),
|
|
909
|
+
},
|
|
910
|
+
];
|
|
911
|
+
// Cache layout to prevent thrashing
|
|
912
|
+
this.cachedConstraints = this.getConstraints();
|
|
566
913
|
}
|
|
567
914
|
else if (touches.length === 2) {
|
|
568
915
|
// Two fingers: Pinch
|
|
569
|
-
this.isPinching
|
|
916
|
+
this.isPinching.set(true);
|
|
570
917
|
this.isDragging.set(false); // Stop panning
|
|
571
918
|
this.initialPinchDistance = this.getDistance(touches);
|
|
572
919
|
this.initialScale = this.scale();
|
|
920
|
+
this.initialPinchCenter = this.getCenter(touches);
|
|
921
|
+
this.initialTranslateX = this.translateX();
|
|
922
|
+
this.initialTranslateY = this.translateY();
|
|
573
923
|
// Clear caching on pinch (scale changes will invalidate limits)
|
|
574
924
|
this.cachedConstraints = null;
|
|
575
925
|
// Prevent default to avoid browser zoom
|
|
@@ -578,7 +928,7 @@ class ImagesPreviewComponent {
|
|
|
578
928
|
}
|
|
579
929
|
}
|
|
580
930
|
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",
|
|
931
|
+
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
932
|
<div
|
|
583
933
|
class="overlay"
|
|
584
934
|
[@fadeInOut]
|
|
@@ -587,6 +937,7 @@ class ImagesPreviewComponent {
|
|
|
587
937
|
tabindex="0"
|
|
588
938
|
role="button"
|
|
589
939
|
aria-label="Close preview overlay"
|
|
940
|
+
[style.background-color]="overlayBackground()"
|
|
590
941
|
>
|
|
591
942
|
<!-- Custom Template Support -->
|
|
592
943
|
@if (customTemplate(); as template) {
|
|
@@ -595,7 +946,7 @@ class ImagesPreviewComponent {
|
|
|
595
946
|
></ng-container>
|
|
596
947
|
} @else {
|
|
597
948
|
<!-- Loading -->
|
|
598
|
-
@if (
|
|
949
|
+
@if (!loadedImages().has(src()) && !hasError()) {
|
|
599
950
|
<div class="loader">Loading...</div>
|
|
600
951
|
}
|
|
601
952
|
|
|
@@ -611,27 +962,32 @@ class ImagesPreviewComponent {
|
|
|
611
962
|
(touchstart)="onTouchStart($event)"
|
|
612
963
|
tabindex="-1"
|
|
613
964
|
>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
965
|
+
@for (item of activeBuffer(); track item.src) {
|
|
966
|
+
<img
|
|
967
|
+
#imgRef
|
|
968
|
+
[src]="item.src"
|
|
969
|
+
[srcset]="item.srcset || ''"
|
|
970
|
+
[class.opacity-0]="item.offset === 0 && (!loadedImages().has(item.src) || hasError())"
|
|
971
|
+
class="preview-image"
|
|
972
|
+
[class.dragging]="isDragging()"
|
|
973
|
+
[class.inertia]="isInertia()"
|
|
974
|
+
[class.pinching]="isPinching()"
|
|
975
|
+
[class.zoom-in]="scale() === 1"
|
|
976
|
+
[class.zoom-out]="scale() > 1"
|
|
977
|
+
[class.hidden]="item.offset !== 0 && scale() > 1"
|
|
978
|
+
[style.transform]="getTransform(item.offset)"
|
|
979
|
+
(load)="onImageLoad(item.src)"
|
|
980
|
+
(error)="onImageError(item.src)"
|
|
981
|
+
(mousedown)="onMouseDown($event)"
|
|
982
|
+
(click)="$event.stopPropagation()"
|
|
983
|
+
draggable="false"
|
|
984
|
+
alt="Preview"
|
|
985
|
+
/>
|
|
986
|
+
}
|
|
630
987
|
</div>
|
|
631
988
|
|
|
632
989
|
<!-- 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>
|
|
990
|
+
<button class="close-btn" (click)="close()" aria-label="Close preview" [innerHTML]="icons.close">
|
|
635
991
|
</button>
|
|
636
992
|
|
|
637
993
|
<!-- Navigation -->
|
|
@@ -643,8 +999,8 @@ class ImagesPreviewComponent {
|
|
|
643
999
|
(click)="prev(); $event.stopPropagation()"
|
|
644
1000
|
(mousedown)="$event.stopPropagation()"
|
|
645
1001
|
aria-label="Previous image"
|
|
1002
|
+
[innerHTML]="icons.prev"
|
|
646
1003
|
>
|
|
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
1004
|
</button>
|
|
649
1005
|
}
|
|
650
1006
|
<!-- Check if not last -->
|
|
@@ -654,8 +1010,8 @@ class ImagesPreviewComponent {
|
|
|
654
1010
|
(click)="next(); $event.stopPropagation()"
|
|
655
1011
|
(mousedown)="$event.stopPropagation()"
|
|
656
1012
|
aria-label="Next image"
|
|
1013
|
+
[innerHTML]="icons.next"
|
|
657
1014
|
>
|
|
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
1015
|
</button>
|
|
660
1016
|
}
|
|
661
1017
|
|
|
@@ -664,40 +1020,61 @@ class ImagesPreviewComponent {
|
|
|
664
1020
|
}
|
|
665
1021
|
|
|
666
1022
|
<!-- Toolbar -->
|
|
1023
|
+
<!-- Toolbar -->
|
|
1024
|
+
@if (showToolbar()) {
|
|
667
1025
|
<div
|
|
668
1026
|
class="toolbar"
|
|
669
1027
|
(click)="$event.stopPropagation()"
|
|
670
1028
|
(keydown)="onToolbarKey($event)"
|
|
671
1029
|
tabindex="0"
|
|
672
1030
|
>
|
|
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
|
-
|
|
1031
|
+
<!-- Flip -->
|
|
1032
|
+
@if (toolbarConfig().showFlip) {
|
|
1033
|
+
<button class="toolbar-btn" (click)="flipHorizontal()" aria-label="Flip Horizontal" [innerHTML]="icons.flipH">
|
|
1034
|
+
</button>
|
|
1035
|
+
<button class="toolbar-btn" (click)="flipVertical()" aria-label="Flip Vertical" [innerHTML]="icons.flipV">
|
|
1036
|
+
</button>
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
<!-- Extensions (Custom Buttons) -->
|
|
1040
|
+
@if (toolbarExtensions()) {
|
|
1041
|
+
<ng-container *ngTemplateOutlet="toolbarExtensions(); context: { $implicit: images()![currentIndex()] }"></ng-container>
|
|
1042
|
+
}
|
|
1043
|
+
<!-- Rotate -->
|
|
1044
|
+
@if (toolbarConfig().showRotate) {
|
|
1045
|
+
<button class="toolbar-btn" (click)="rotateLeft()" aria-label="Rotate Left" [innerHTML]="icons.rotateLeft">
|
|
1046
|
+
</button>
|
|
1047
|
+
<button class="toolbar-btn" (click)="rotateRight()" aria-label="Rotate Right" [innerHTML]="icons.rotateRight">
|
|
1048
|
+
</button>
|
|
1049
|
+
}
|
|
1050
|
+
<!-- Zoom -->
|
|
1051
|
+
@if (toolbarConfig().showZoom) {
|
|
1052
|
+
<button class="toolbar-btn" (click)="zoomOut()" aria-label="Zoom Out" [innerHTML]="icons.zoomOut">
|
|
1053
|
+
</button>
|
|
1054
|
+
<button class="toolbar-btn" (click)="zoomIn()" aria-label="Zoom In" [innerHTML]="icons.zoomIn">
|
|
1055
|
+
</button>
|
|
1056
|
+
}
|
|
1057
|
+
</div>
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
<!-- Thumbnails -->
|
|
1062
|
+
@if (images() && images()!.length > 1 && showThumbnails()) {
|
|
1063
|
+
<div class="thumbnail-strip" (click)="$event.stopPropagation()">
|
|
1064
|
+
@for (img of images(); track img; let i = $index) {
|
|
1065
|
+
<div
|
|
1066
|
+
#thumbRef
|
|
1067
|
+
class="thumbnail-item"
|
|
1068
|
+
[class.active]="i === currentIndex()"
|
|
1069
|
+
(click)="jumpTo(i)"
|
|
1070
|
+
>
|
|
1071
|
+
<img [src]="img" loading="lazy" alt="Thumbnail">
|
|
1072
|
+
</div>
|
|
1073
|
+
}
|
|
697
1074
|
</div>
|
|
698
1075
|
}
|
|
699
1076
|
</div>
|
|
700
|
-
`, isInline: true, styles: [":host{display:block}.overlay{position:fixed;inset:0;z-index:
|
|
1077
|
+
`, 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
1078
|
trigger('fadeInOut', [
|
|
702
1079
|
transition(':enter', [
|
|
703
1080
|
style({ opacity: 0 }),
|
|
@@ -718,6 +1095,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
718
1095
|
tabindex="0"
|
|
719
1096
|
role="button"
|
|
720
1097
|
aria-label="Close preview overlay"
|
|
1098
|
+
[style.background-color]="overlayBackground()"
|
|
721
1099
|
>
|
|
722
1100
|
<!-- Custom Template Support -->
|
|
723
1101
|
@if (customTemplate(); as template) {
|
|
@@ -726,7 +1104,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
726
1104
|
></ng-container>
|
|
727
1105
|
} @else {
|
|
728
1106
|
<!-- Loading -->
|
|
729
|
-
@if (
|
|
1107
|
+
@if (!loadedImages().has(src()) && !hasError()) {
|
|
730
1108
|
<div class="loader">Loading...</div>
|
|
731
1109
|
}
|
|
732
1110
|
|
|
@@ -742,27 +1120,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
742
1120
|
(touchstart)="onTouchStart($event)"
|
|
743
1121
|
tabindex="-1"
|
|
744
1122
|
>
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1123
|
+
@for (item of activeBuffer(); track item.src) {
|
|
1124
|
+
<img
|
|
1125
|
+
#imgRef
|
|
1126
|
+
[src]="item.src"
|
|
1127
|
+
[srcset]="item.srcset || ''"
|
|
1128
|
+
[class.opacity-0]="item.offset === 0 && (!loadedImages().has(item.src) || hasError())"
|
|
1129
|
+
class="preview-image"
|
|
1130
|
+
[class.dragging]="isDragging()"
|
|
1131
|
+
[class.inertia]="isInertia()"
|
|
1132
|
+
[class.pinching]="isPinching()"
|
|
1133
|
+
[class.zoom-in]="scale() === 1"
|
|
1134
|
+
[class.zoom-out]="scale() > 1"
|
|
1135
|
+
[class.hidden]="item.offset !== 0 && scale() > 1"
|
|
1136
|
+
[style.transform]="getTransform(item.offset)"
|
|
1137
|
+
(load)="onImageLoad(item.src)"
|
|
1138
|
+
(error)="onImageError(item.src)"
|
|
1139
|
+
(mousedown)="onMouseDown($event)"
|
|
1140
|
+
(click)="$event.stopPropagation()"
|
|
1141
|
+
draggable="false"
|
|
1142
|
+
alt="Preview"
|
|
1143
|
+
/>
|
|
1144
|
+
}
|
|
761
1145
|
</div>
|
|
762
1146
|
|
|
763
1147
|
<!-- 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>
|
|
1148
|
+
<button class="close-btn" (click)="close()" aria-label="Close preview" [innerHTML]="icons.close">
|
|
766
1149
|
</button>
|
|
767
1150
|
|
|
768
1151
|
<!-- Navigation -->
|
|
@@ -774,8 +1157,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
774
1157
|
(click)="prev(); $event.stopPropagation()"
|
|
775
1158
|
(mousedown)="$event.stopPropagation()"
|
|
776
1159
|
aria-label="Previous image"
|
|
1160
|
+
[innerHTML]="icons.prev"
|
|
777
1161
|
>
|
|
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
1162
|
</button>
|
|
780
1163
|
}
|
|
781
1164
|
<!-- Check if not last -->
|
|
@@ -785,8 +1168,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
785
1168
|
(click)="next(); $event.stopPropagation()"
|
|
786
1169
|
(mousedown)="$event.stopPropagation()"
|
|
787
1170
|
aria-label="Next image"
|
|
1171
|
+
[innerHTML]="icons.next"
|
|
788
1172
|
>
|
|
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
1173
|
</button>
|
|
791
1174
|
}
|
|
792
1175
|
|
|
@@ -795,36 +1178,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
795
1178
|
}
|
|
796
1179
|
|
|
797
1180
|
<!-- Toolbar -->
|
|
1181
|
+
<!-- Toolbar -->
|
|
1182
|
+
@if (showToolbar()) {
|
|
798
1183
|
<div
|
|
799
1184
|
class="toolbar"
|
|
800
1185
|
(click)="$event.stopPropagation()"
|
|
801
1186
|
(keydown)="onToolbarKey($event)"
|
|
802
1187
|
tabindex="0"
|
|
803
1188
|
>
|
|
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
|
-
|
|
1189
|
+
<!-- Flip -->
|
|
1190
|
+
@if (toolbarConfig().showFlip) {
|
|
1191
|
+
<button class="toolbar-btn" (click)="flipHorizontal()" aria-label="Flip Horizontal" [innerHTML]="icons.flipH">
|
|
1192
|
+
</button>
|
|
1193
|
+
<button class="toolbar-btn" (click)="flipVertical()" aria-label="Flip Vertical" [innerHTML]="icons.flipV">
|
|
1194
|
+
</button>
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
<!-- Extensions (Custom Buttons) -->
|
|
1198
|
+
@if (toolbarExtensions()) {
|
|
1199
|
+
<ng-container *ngTemplateOutlet="toolbarExtensions(); context: { $implicit: images()![currentIndex()] }"></ng-container>
|
|
1200
|
+
}
|
|
1201
|
+
<!-- Rotate -->
|
|
1202
|
+
@if (toolbarConfig().showRotate) {
|
|
1203
|
+
<button class="toolbar-btn" (click)="rotateLeft()" aria-label="Rotate Left" [innerHTML]="icons.rotateLeft">
|
|
1204
|
+
</button>
|
|
1205
|
+
<button class="toolbar-btn" (click)="rotateRight()" aria-label="Rotate Right" [innerHTML]="icons.rotateRight">
|
|
1206
|
+
</button>
|
|
1207
|
+
}
|
|
1208
|
+
<!-- Zoom -->
|
|
1209
|
+
@if (toolbarConfig().showZoom) {
|
|
1210
|
+
<button class="toolbar-btn" (click)="zoomOut()" aria-label="Zoom Out" [innerHTML]="icons.zoomOut">
|
|
1211
|
+
</button>
|
|
1212
|
+
<button class="toolbar-btn" (click)="zoomIn()" aria-label="Zoom In" [innerHTML]="icons.zoomIn">
|
|
1213
|
+
</button>
|
|
1214
|
+
}
|
|
1215
|
+
</div>
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
<!-- Thumbnails -->
|
|
1220
|
+
@if (images() && images()!.length > 1 && showThumbnails()) {
|
|
1221
|
+
<div class="thumbnail-strip" (click)="$event.stopPropagation()">
|
|
1222
|
+
@for (img of images(); track img; let i = $index) {
|
|
1223
|
+
<div
|
|
1224
|
+
#thumbRef
|
|
1225
|
+
class="thumbnail-item"
|
|
1226
|
+
[class.active]="i === currentIndex()"
|
|
1227
|
+
(click)="jumpTo(i)"
|
|
1228
|
+
>
|
|
1229
|
+
<img [src]="img" loading="lazy" alt="Thumbnail">
|
|
1230
|
+
</div>
|
|
1231
|
+
}
|
|
828
1232
|
</div>
|
|
829
1233
|
}
|
|
830
1234
|
</div>
|
|
@@ -843,8 +1247,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
843
1247
|
'(document:keydown.arrowleft)': 'prev()',
|
|
844
1248
|
'(document:keydown.arrowright)': 'next()',
|
|
845
1249
|
'(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 }] }],
|
|
1250
|
+
}, 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"] }]
|
|
1251
|
+
}], 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
1252
|
type: HostListener,
|
|
849
1253
|
args: ['document:keydown.escape']
|
|
850
1254
|
}], onMouseMove: [{
|
|
@@ -852,10 +1256,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
852
1256
|
args: ['document:mousemove', ['$event']]
|
|
853
1257
|
}] } });
|
|
854
1258
|
|
|
1259
|
+
/**
|
|
1260
|
+
* Directive to open the image preview.
|
|
1261
|
+
* Can be used on any element, but auto-detects `src` on `img` or nested `img`.
|
|
1262
|
+
*/
|
|
855
1263
|
class ImagesPreviewDirective {
|
|
1264
|
+
/**
|
|
1265
|
+
* High resolution image source to display in preview.
|
|
1266
|
+
* If empty, tries to read `src` from host element or first child `img`.
|
|
1267
|
+
*/
|
|
856
1268
|
highResSrc = '';
|
|
1269
|
+
/**
|
|
1270
|
+
* List of images for gallery navigation.
|
|
1271
|
+
* If provided, enables previous/next navigation buttons.
|
|
1272
|
+
*/
|
|
857
1273
|
previewImages = [];
|
|
1274
|
+
/**
|
|
1275
|
+
* Custom template to use for the preview.
|
|
1276
|
+
* If provided, overrides the default viewer UI.
|
|
1277
|
+
*/
|
|
858
1278
|
previewTemplate;
|
|
1279
|
+
/**
|
|
1280
|
+
* Configuration for the toolbar buttons.
|
|
1281
|
+
*/
|
|
1282
|
+
toolbarConfig;
|
|
1283
|
+
/**
|
|
1284
|
+
* Optional srcset for the single image.
|
|
1285
|
+
*/
|
|
1286
|
+
srcset;
|
|
1287
|
+
/**
|
|
1288
|
+
* List of srcsets corresponding to the `previewImages` array.
|
|
1289
|
+
*/
|
|
1290
|
+
/**
|
|
1291
|
+
* List of srcsets corresponding to the `previewImages` array.
|
|
1292
|
+
*/
|
|
1293
|
+
srcsets;
|
|
1294
|
+
/**
|
|
1295
|
+
* Whether to show the thumbnail strip.
|
|
1296
|
+
* @default true
|
|
1297
|
+
*/
|
|
1298
|
+
showThumbnails = true;
|
|
1299
|
+
/**
|
|
1300
|
+
* Whether to show the toolbar.
|
|
1301
|
+
* @default true
|
|
1302
|
+
*/
|
|
1303
|
+
showToolbar = true;
|
|
1304
|
+
/**
|
|
1305
|
+
* Custom template to render in the toolbar (e.g. for download buttons).
|
|
1306
|
+
*/
|
|
1307
|
+
toolbarExtensions = null;
|
|
1308
|
+
/**
|
|
1309
|
+
* Type guard helper for strict template type checking.
|
|
1310
|
+
* Allows Angular Language Service to infer types in `previewTemplate`.
|
|
1311
|
+
*/
|
|
1312
|
+
static ngTemplateContextGuard(directive, context) {
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
859
1315
|
componentRef = null;
|
|
860
1316
|
appRef = inject(ApplicationRef);
|
|
861
1317
|
injector = inject(EnvironmentInjector);
|
|
@@ -878,11 +1334,12 @@ class ImagesPreviewDirective {
|
|
|
878
1334
|
}
|
|
879
1335
|
src = src || '';
|
|
880
1336
|
if (src) {
|
|
881
|
-
|
|
1337
|
+
const rect = hostEl.getBoundingClientRect();
|
|
1338
|
+
this.openPreview(src, rect);
|
|
882
1339
|
}
|
|
883
1340
|
}
|
|
884
1341
|
cursor = 'pointer';
|
|
885
|
-
openPreview(src) {
|
|
1342
|
+
openPreview(src, rect) {
|
|
886
1343
|
if (!isPlatformBrowser(this.platformId))
|
|
887
1344
|
return;
|
|
888
1345
|
// Create Component
|
|
@@ -891,6 +1348,7 @@ class ImagesPreviewDirective {
|
|
|
891
1348
|
});
|
|
892
1349
|
// Set Inputs
|
|
893
1350
|
this.componentRef.setInput('src', src);
|
|
1351
|
+
this.componentRef.setInput('openerRect', rect);
|
|
894
1352
|
if (this.previewImages.length > 0) {
|
|
895
1353
|
this.componentRef.setInput('images', this.previewImages);
|
|
896
1354
|
const index = this.previewImages.indexOf(src);
|
|
@@ -899,6 +1357,18 @@ class ImagesPreviewDirective {
|
|
|
899
1357
|
if (this.previewTemplate) {
|
|
900
1358
|
this.componentRef.setInput('customTemplate', this.previewTemplate);
|
|
901
1359
|
}
|
|
1360
|
+
if (this.toolbarConfig) {
|
|
1361
|
+
this.componentRef.setInput('toolbarConfig', this.toolbarConfig);
|
|
1362
|
+
}
|
|
1363
|
+
if (this.srcset) {
|
|
1364
|
+
this.componentRef.setInput('srcset', this.srcset);
|
|
1365
|
+
}
|
|
1366
|
+
if (this.srcsets) {
|
|
1367
|
+
this.componentRef.setInput('srcsets', this.srcsets);
|
|
1368
|
+
}
|
|
1369
|
+
this.componentRef.setInput('showThumbnails', this.showThumbnails);
|
|
1370
|
+
this.componentRef.setInput('showToolbar', this.showToolbar);
|
|
1371
|
+
this.componentRef.setInput('toolbarExtensions', this.toolbarExtensions);
|
|
902
1372
|
// Set Callbacks
|
|
903
1373
|
this.componentRef.instance.closeCallback = () => this.destroyPreview();
|
|
904
1374
|
// Attach to App
|
|
@@ -918,7 +1388,7 @@ class ImagesPreviewDirective {
|
|
|
918
1388
|
this.destroyPreview();
|
|
919
1389
|
}
|
|
920
1390
|
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 });
|
|
1391
|
+
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
1392
|
}
|
|
923
1393
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: ImagesPreviewDirective, decorators: [{
|
|
924
1394
|
type: Directive,
|
|
@@ -933,6 +1403,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
933
1403
|
type: Input
|
|
934
1404
|
}], previewTemplate: [{
|
|
935
1405
|
type: Input
|
|
1406
|
+
}], toolbarConfig: [{
|
|
1407
|
+
type: Input
|
|
1408
|
+
}], srcset: [{
|
|
1409
|
+
type: Input
|
|
1410
|
+
}], srcsets: [{
|
|
1411
|
+
type: Input
|
|
1412
|
+
}], showThumbnails: [{
|
|
1413
|
+
type: Input
|
|
1414
|
+
}], showToolbar: [{
|
|
1415
|
+
type: Input
|
|
1416
|
+
}], toolbarExtensions: [{
|
|
1417
|
+
type: Input
|
|
936
1418
|
}], onClick: [{
|
|
937
1419
|
type: HostListener,
|
|
938
1420
|
args: ['click', ['$event']]
|
|
@@ -941,6 +1423,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
941
1423
|
args: ['style.cursor']
|
|
942
1424
|
}] } });
|
|
943
1425
|
|
|
1426
|
+
class NgImagesPreviewModule {
|
|
1427
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: NgImagesPreviewModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
1428
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.7", ngImport: i0, type: NgImagesPreviewModule, imports: [ImagesPreviewComponent, ImagesPreviewDirective], exports: [ImagesPreviewComponent, ImagesPreviewDirective] });
|
|
1429
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: NgImagesPreviewModule, imports: [ImagesPreviewComponent] });
|
|
1430
|
+
}
|
|
1431
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: NgImagesPreviewModule, decorators: [{
|
|
1432
|
+
type: NgModule,
|
|
1433
|
+
args: [{
|
|
1434
|
+
imports: [ImagesPreviewComponent, ImagesPreviewDirective],
|
|
1435
|
+
exports: [ImagesPreviewComponent, ImagesPreviewDirective]
|
|
1436
|
+
}]
|
|
1437
|
+
}] });
|
|
1438
|
+
|
|
944
1439
|
/*
|
|
945
1440
|
* Public API Surface of ng-images-preview
|
|
946
1441
|
*/
|
|
@@ -949,5 +1444,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImpor
|
|
|
949
1444
|
* Generated bundle index. Do not edit.
|
|
950
1445
|
*/
|
|
951
1446
|
|
|
952
|
-
export { ImagesPreviewComponent, ImagesPreviewDirective };
|
|
1447
|
+
export { ImagesPreviewComponent, ImagesPreviewDirective, NgImagesPreviewModule };
|
|
953
1448
|
//# sourceMappingURL=ng-images-preview.mjs.map
|