ng-images-preview 1.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.
@@ -0,0 +1,971 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, viewChild, signal, computed, effect, HostListener, ChangeDetectionStrategy, Component, inject, ApplicationRef, EnvironmentInjector, ElementRef, createComponent, Input, Directive } from '@angular/core';
3
+ import * as i1 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import { trigger, transition, style, animate } from '@angular/animations';
6
+
7
+ class ImagesPreviewComponent {
8
+ src = input.required(...(ngDevMode ? [{ debugName: "src" }] : []));
9
+ images = input(...(ngDevMode ? [undefined, { debugName: "images" }] : []));
10
+ initialIndex = input(0, ...(ngDevMode ? [{ debugName: "initialIndex" }] : []));
11
+ customTemplate = input(...(ngDevMode ? [undefined, { debugName: "customTemplate" }] : []));
12
+ closeCallback = () => {
13
+ /* noop */
14
+ };
15
+ imgRef = viewChild('imgRef', ...(ngDevMode ? [{ debugName: "imgRef" }] : []));
16
+ // State signals
17
+ currentIndex = signal(0, ...(ngDevMode ? [{ debugName: "currentIndex" }] : []));
18
+ isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
19
+ hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
20
+ activeSrc = computed(() => {
21
+ const imgs = this.images();
22
+ if (imgs && imgs.length > 0) {
23
+ return imgs[this.currentIndex()];
24
+ }
25
+ return this.src();
26
+ }, ...(ngDevMode ? [{ debugName: "activeSrc" }] : []));
27
+ constructor() {
28
+ // defined in class body usually, but here to show logical grouping
29
+ effect(() => {
30
+ // Initialize index from input
31
+ this.currentIndex.set(this.initialIndex());
32
+ }, { allowSignalWrites: true });
33
+ effect(() => {
34
+ // Reset state when index changes
35
+ this.currentIndex();
36
+ this.isLoading.set(true);
37
+ this.hasError.set(false);
38
+ this.reset(); // Reset zoom/rotate
39
+ }, { allowSignalWrites: true });
40
+ }
41
+ scale = signal(1, ...(ngDevMode ? [{ debugName: "scale" }] : []));
42
+ translateX = signal(0, ...(ngDevMode ? [{ debugName: "translateX" }] : []));
43
+ translateY = signal(0, ...(ngDevMode ? [{ debugName: "translateY" }] : []));
44
+ rotate = signal(0, ...(ngDevMode ? [{ debugName: "rotate" }] : []));
45
+ flipH = signal(false, ...(ngDevMode ? [{ debugName: "flipH" }] : []));
46
+ flipV = signal(false, ...(ngDevMode ? [{ debugName: "flipV" }] : []));
47
+ isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
48
+ // Touch state
49
+ initialPinchDistance = 0;
50
+ initialScale = 1;
51
+ isPinching = false;
52
+ // Computed state object for template
53
+ state = computed(() => ({
54
+ src: this.src(),
55
+ scale: this.scale(),
56
+ rotate: this.rotate(),
57
+ flipH: this.flipH(),
58
+ flipV: this.flipV(),
59
+ isLoading: this.isLoading(),
60
+ hasError: this.hasError(),
61
+ }), ...(ngDevMode ? [{ debugName: "state" }] : []));
62
+ // Actions object for template
63
+ actions = {
64
+ zoomIn: () => this.zoomIn(),
65
+ zoomOut: () => this.zoomOut(),
66
+ rotateLeft: () => this.rotateLeft(),
67
+ rotateRight: () => this.rotateRight(),
68
+ flipHorizontal: () => this.flipHorizontal(),
69
+ flipVertical: () => this.flipVertical(),
70
+ reset: () => this.reset(),
71
+ close: () => this.close(),
72
+ };
73
+ MIN_SCALE = 0.5;
74
+ MAX_SCALE = 5;
75
+ ZOOM_STEP = 0.5;
76
+ startX = 0;
77
+ startY = 0;
78
+ lastTranslateX = 0;
79
+ lastTranslateY = 0;
80
+ // Physics state
81
+ velocityX = 0;
82
+ velocityY = 0;
83
+ touchHistory = [];
84
+ cachedConstraints = null;
85
+ lastTimestamp = 0;
86
+ rafId = null;
87
+ FRICTION = 0.92; // Heavier feel
88
+ VELOCITY_THRESHOLD = 0.01;
89
+ MAX_VELOCITY = 3; // Cap speed to prevent teleporting
90
+ transformStyle = computed(() => {
91
+ const scale = this.scale();
92
+ const x = this.translateX();
93
+ const y = this.translateY();
94
+ const rotate = this.rotate();
95
+ const scaleX = this.flipH() ? -1 : 1;
96
+ const scaleY = this.flipV() ? -1 : 1;
97
+ return `translate3d(${x}px, ${y}px, 0) scale(${scale}) rotate(${rotate}deg) scaleX(${scaleX}) scaleY(${scaleY})`;
98
+ }, ...(ngDevMode ? [{ debugName: "transformStyle" }] : []));
99
+ onEscape() {
100
+ this.close();
101
+ }
102
+ onMouseMove(event) {
103
+ if (!this.isDragging())
104
+ return;
105
+ event.preventDefault();
106
+ const deltaX = event.clientX - this.startX;
107
+ const deltaY = event.clientY - this.startY;
108
+ let nextX = this.lastTranslateX + deltaX;
109
+ let nextY = this.lastTranslateY + deltaY;
110
+ // Apply strict clamping during move
111
+ this.applyMoveConstraints(nextX, nextY);
112
+ }
113
+ applyMoveConstraints(nextX, nextY) {
114
+ const img = this.imgRef()?.nativeElement;
115
+ if (!img) {
116
+ this.translateX.set(nextX);
117
+ this.translateY.set(nextY);
118
+ return;
119
+ }
120
+ const scale = this.scale();
121
+ const rotate = Math.abs(this.rotate() % 180);
122
+ const isRotated = rotate === 90;
123
+ const baseWidth = img.offsetWidth;
124
+ const baseHeight = img.offsetHeight;
125
+ const currentWidth = (isRotated ? baseHeight : baseWidth) * scale;
126
+ const currentHeight = (isRotated ? baseWidth : baseHeight) * scale;
127
+ const viewportWidth = window.innerWidth;
128
+ const viewportHeight = window.innerHeight;
129
+ const maxTranslateX = Math.max(0, (currentWidth - viewportWidth) / 2);
130
+ const maxTranslateY = Math.max(0, (currentHeight - viewportHeight) / 2);
131
+ // Clamp
132
+ const clampedX = Math.max(-maxTranslateX, Math.min(maxTranslateX, nextX));
133
+ const clampedY = Math.max(-maxTranslateY, Math.min(maxTranslateY, nextY));
134
+ this.translateX.set(clampedX);
135
+ this.translateY.set(clampedY);
136
+ }
137
+ onTouchMove(event) {
138
+ if (!this.isDragging() && !this.isPinching)
139
+ return;
140
+ // Prevent default to stop page scrolling/zooming
141
+ if (event.cancelable) {
142
+ event.preventDefault();
143
+ }
144
+ const touches = event.touches;
145
+ // One finger: Pan
146
+ if (touches.length === 1 && this.isDragging() && !this.isPinching) {
147
+ const now = Date.now();
148
+ // Add point to history
149
+ this.touchHistory.push({
150
+ x: touches[0].clientX,
151
+ y: touches[0].clientY,
152
+ time: now,
153
+ });
154
+ // Prune history (keep last 100ms)
155
+ const cutoff = now - 100;
156
+ while (this.touchHistory.length > 0 && this.touchHistory[0].time < cutoff) {
157
+ this.touchHistory.shift();
158
+ }
159
+ const deltaX = touches[0].clientX - this.lastTouchX;
160
+ const deltaY = touches[0].clientY - this.lastTouchY;
161
+ // Note: We do NOT calculate velocity here anymore to avoid noise.
162
+ // We verify velocity at TouchEnd using history.
163
+ const currentX = this.translateX();
164
+ const currentY = this.translateY();
165
+ let nextX = currentX + deltaX;
166
+ let nextY = currentY + deltaY;
167
+ // Apply rubber banding if out of bounds
168
+ // Use cached constraints to avoid reflows
169
+ const constraints = this.cachedConstraints || this.getConstraints();
170
+ if (nextX > constraints.maxX) {
171
+ nextX = constraints.maxX + (nextX - constraints.maxX) * 0.5;
172
+ }
173
+ else if (nextX < -constraints.maxX) {
174
+ nextX = -constraints.maxX + (nextX + constraints.maxX) * 0.5;
175
+ }
176
+ if (nextY > constraints.maxY) {
177
+ nextY = constraints.maxY + (nextY - constraints.maxY) * 0.5;
178
+ }
179
+ else if (nextY < -constraints.maxY) {
180
+ nextY = -constraints.maxY + (nextY + constraints.maxY) * 0.5;
181
+ }
182
+ this.translateX.set(nextX);
183
+ this.translateY.set(nextY);
184
+ this.lastTouchX = touches[0].clientX;
185
+ this.lastTouchY = touches[0].clientY;
186
+ }
187
+ // Two fingers: Pinch Zoom
188
+ if (touches.length === 2) {
189
+ const distance = this.getDistance(touches);
190
+ if (this.initialPinchDistance > 0) {
191
+ const scaleFactor = distance / this.initialPinchDistance;
192
+ const newScale = Math.min(Math.max(this.initialScale * scaleFactor, this.MIN_SCALE), this.MAX_SCALE);
193
+ this.scale.set(newScale);
194
+ // Re-clamp position after zoom
195
+ this.clampPosition();
196
+ }
197
+ }
198
+ }
199
+ getConstraints() {
200
+ // If we have a valid cache during interaction, use it to avoid reflows
201
+ // The instruction says: "So `getConstraints` stays as is (calculates fresh)."
202
+ // So, this method should always calculate fresh.
203
+ // The `cachedConstraints` property is used *outside* this method.
204
+ const img = this.imgRef()?.nativeElement;
205
+ if (!img)
206
+ return { maxX: 0, maxY: 0 };
207
+ const scale = this.scale();
208
+ const rotate = Math.abs(this.rotate() % 180);
209
+ const isRotated = rotate === 90;
210
+ const baseWidth = img.offsetWidth;
211
+ const baseHeight = img.offsetHeight;
212
+ const currentWidth = (isRotated ? baseHeight : baseWidth) * scale;
213
+ const currentHeight = (isRotated ? baseWidth : baseHeight) * scale;
214
+ const viewportWidth = window.innerWidth;
215
+ const viewportHeight = window.innerHeight;
216
+ return {
217
+ maxX: Math.max(0, (currentWidth - viewportWidth) / 2),
218
+ maxY: Math.max(0, (currentHeight - viewportHeight) / 2),
219
+ };
220
+ }
221
+ clampPosition() {
222
+ const img = this.imgRef()?.nativeElement;
223
+ if (!img)
224
+ return;
225
+ const scale = this.scale();
226
+ const rotate = Math.abs(this.rotate() % 180);
227
+ const isRotated = rotate === 90;
228
+ const baseWidth = img.offsetWidth;
229
+ const baseHeight = img.offsetHeight;
230
+ // Effective dimensions after rotation
231
+ const currentWidth = (isRotated ? baseHeight : baseWidth) * scale;
232
+ const currentHeight = (isRotated ? baseWidth : baseHeight) * scale;
233
+ const viewportWidth = window.innerWidth;
234
+ const viewportHeight = window.innerHeight;
235
+ const maxTranslateX = Math.max(0, (currentWidth - viewportWidth) / 2);
236
+ const maxTranslateY = Math.max(0, (currentHeight - viewportHeight) / 2);
237
+ // Clamp logic
238
+ const currentX = this.translateX();
239
+ const currentY = this.translateY();
240
+ let newX = currentX;
241
+ let newY = currentY;
242
+ if (Math.abs(currentX) > maxTranslateX) {
243
+ newX = Math.sign(currentX) * maxTranslateX;
244
+ }
245
+ if (Math.abs(currentY) > maxTranslateY) {
246
+ newY = Math.sign(currentY) * maxTranslateY;
247
+ }
248
+ // If image is smaller than viewport, force center (0)
249
+ if (currentWidth <= viewportWidth)
250
+ newX = 0;
251
+ if (currentHeight <= viewportHeight)
252
+ newY = 0;
253
+ if (newX !== currentX)
254
+ this.translateX.set(newX);
255
+ if (newY !== currentY)
256
+ this.translateY.set(newY);
257
+ }
258
+ startInertia() {
259
+ let lastTime = Date.now();
260
+ const step = () => {
261
+ if (!this.isDragging() &&
262
+ (Math.abs(this.velocityX) > this.VELOCITY_THRESHOLD ||
263
+ Math.abs(this.velocityY) > this.VELOCITY_THRESHOLD)) {
264
+ const now = Date.now();
265
+ const dt = Math.min(now - lastTime, 64); // Cap dt to avoid huge jumps if lag
266
+ lastTime = now;
267
+ if (dt === 0) {
268
+ // Skip if 0ms passed (can happen on fast screens)
269
+ this.rafId = requestAnimationFrame(step);
270
+ return;
271
+ }
272
+ // Clamp max velocity (just in case)
273
+ this.velocityX = Math.max(-this.MAX_VELOCITY, Math.min(this.MAX_VELOCITY, this.velocityX));
274
+ this.velocityY = Math.max(-this.MAX_VELOCITY, Math.min(this.MAX_VELOCITY, this.velocityY));
275
+ // Time-based friction
276
+ // Standard friction is 0.95 per 16ms frame
277
+ const frictionFactor = Math.pow(this.FRICTION, dt / 16);
278
+ this.velocityX *= frictionFactor;
279
+ this.velocityY *= frictionFactor;
280
+ let nextX = this.translateX() + this.velocityX * dt;
281
+ let nextY = this.translateY() + this.velocityY * dt;
282
+ // Check bounds during inertia
283
+ // Hard stop/bounce logic can be improved here if needed
284
+ const constraints = this.cachedConstraints || this.getConstraints();
285
+ if (nextX > constraints.maxX) {
286
+ nextX = constraints.maxX;
287
+ this.velocityX = 0;
288
+ }
289
+ else if (nextX < -constraints.maxX) {
290
+ nextX = -constraints.maxX;
291
+ this.velocityX = 0;
292
+ }
293
+ if (nextY > constraints.maxY) {
294
+ nextY = constraints.maxY;
295
+ this.velocityY = 0;
296
+ }
297
+ else if (nextY < -constraints.maxY) {
298
+ nextY = -constraints.maxY;
299
+ this.velocityY = 0;
300
+ }
301
+ this.translateX.set(nextX);
302
+ this.translateY.set(nextY);
303
+ this.rafId = requestAnimationFrame(step);
304
+ }
305
+ else {
306
+ this.stopInertia();
307
+ // Clear cache when movement stops
308
+ // The instruction says to clear it in onTouchEnd or reset/scale changes.
309
+ // So, no need to clear it here.
310
+ }
311
+ };
312
+ this.rafId = requestAnimationFrame(step);
313
+ }
314
+ stopInertia() {
315
+ if (this.rafId) {
316
+ cancelAnimationFrame(this.rafId);
317
+ this.rafId = null;
318
+ }
319
+ }
320
+ snapBack() {
321
+ const constraints = this.cachedConstraints || this.getConstraints();
322
+ const currentX = this.translateX();
323
+ const currentY = this.translateY();
324
+ let targetX = currentX;
325
+ let targetY = currentY;
326
+ if (currentX > constraints.maxX)
327
+ targetX = constraints.maxX;
328
+ if (currentX < -constraints.maxX)
329
+ targetX = -constraints.maxX;
330
+ if (currentY > constraints.maxY)
331
+ targetY = constraints.maxY;
332
+ if (currentY < -constraints.maxY)
333
+ targetY = -constraints.maxY;
334
+ if (targetX !== currentX || targetY !== currentY) {
335
+ // Animate snap back? For now, we rely on CSS transition if not dragging
336
+ // But we turned off transition for .dragging.
337
+ // We need to ensure .dragging is removed (it is in onTouchEnd).
338
+ // CSS transition handles the snap if we just set the value.
339
+ this.translateX.set(targetX);
340
+ this.translateY.set(targetY);
341
+ }
342
+ }
343
+ onToolbarKey(event) {
344
+ event.stopPropagation();
345
+ }
346
+ onOverlayKey(event) {
347
+ if (event.key === 'Enter' || event.key === ' ') {
348
+ this.close();
349
+ }
350
+ }
351
+ onContainerKey(event) {
352
+ // Prevent closing when interacting with image container via keyboard if needed
353
+ event.stopPropagation();
354
+ }
355
+ onMouseUp() {
356
+ this.isDragging.set(false);
357
+ }
358
+ onTouchEnd(event) {
359
+ const touches = event.touches;
360
+ if (touches.length === 0) {
361
+ this.isDragging.set(false);
362
+ // Calculate Release Velocity from History
363
+ const now = Date.now();
364
+ const lastPoint = this.touchHistory[this.touchHistory.length - 1];
365
+ // We want a point from roughly 30-50ms ago to get "launch" direction,
366
+ // but tracking last 100ms.
367
+ // A simple approach: compare last point with oldest point in our (pruned) buffer.
368
+ const oldestPoint = this.touchHistory[0];
369
+ if (lastPoint && oldestPoint && lastPoint !== oldestPoint) {
370
+ const dt = lastPoint.time - oldestPoint.time;
371
+ if (dt > 0) {
372
+ this.velocityX = (lastPoint.x - oldestPoint.x) / dt;
373
+ this.velocityY = (lastPoint.y - oldestPoint.y) / dt;
374
+ }
375
+ }
376
+ else {
377
+ this.velocityX = 0;
378
+ this.velocityY = 0;
379
+ }
380
+ this.isDragging.set(false);
381
+ this.isPinching = false;
382
+ this.initialPinchDistance = 0;
383
+ // Swipe Navigation (at 1x scale)
384
+ if (this.scale() === 1) {
385
+ const x = this.translateX();
386
+ const threshold = 50; // px
387
+ if (x < -threshold) {
388
+ this.next();
389
+ return;
390
+ }
391
+ else if (x > threshold) {
392
+ this.prev();
393
+ return;
394
+ }
395
+ }
396
+ // Check bounds
397
+ const constraints = this.cachedConstraints || this.getConstraints();
398
+ const x = this.translateX();
399
+ const y = this.translateY();
400
+ const outOfBounds = x > constraints.maxX ||
401
+ x < -constraints.maxX ||
402
+ y > constraints.maxY ||
403
+ y < -constraints.maxY;
404
+ if (outOfBounds) {
405
+ this.snapBack();
406
+ this.velocityX = 0;
407
+ this.velocityY = 0;
408
+ this.cachedConstraints = null;
409
+ }
410
+ else {
411
+ this.startInertia();
412
+ }
413
+ }
414
+ else if (touches.length === 1 && this.isPinching) {
415
+ // Transition from pinch to pan
416
+ this.isPinching = false;
417
+ this.isDragging.set(true);
418
+ this.lastTouchX = touches[0].clientX;
419
+ this.lastTouchY = touches[0].clientY;
420
+ // Reset velocity on transition
421
+ this.velocityX = 0;
422
+ this.velocityY = 0;
423
+ this.lastTimestamp = Date.now();
424
+ // Re-cache constraints for new pan interaction
425
+ this.cachedConstraints = this.getConstraints();
426
+ }
427
+ }
428
+ lastTouchX = 0;
429
+ lastTouchY = 0;
430
+ getDistance(touches) {
431
+ return Math.hypot(touches[0].clientX - touches[1].clientX, touches[0].clientY - touches[1].clientY);
432
+ }
433
+ close() {
434
+ this.closeCallback();
435
+ }
436
+ onImageLoad() {
437
+ this.isLoading.set(false);
438
+ this.hasError.set(false);
439
+ }
440
+ onImageError() {
441
+ this.isLoading.set(false);
442
+ this.hasError.set(true);
443
+ }
444
+ // Zoom
445
+ zoomIn() {
446
+ this.scale.update((s) => Math.min(s + this.ZOOM_STEP, this.MAX_SCALE));
447
+ setTimeout(() => this.clampPosition());
448
+ this.cachedConstraints = null; // Invalidate cache on zoom
449
+ }
450
+ zoomOut() {
451
+ this.scale.update((s) => Math.max(s - this.ZOOM_STEP, this.MIN_SCALE));
452
+ setTimeout(() => this.clampPosition());
453
+ this.cachedConstraints = null; // Invalidate cache on zoom
454
+ }
455
+ // Rotate
456
+ rotateLeft() {
457
+ this.rotate.update((r) => r - 90);
458
+ setTimeout(() => this.clampPosition());
459
+ this.cachedConstraints = null; // Invalidate cache on rotate
460
+ }
461
+ rotateRight() {
462
+ this.rotate.update((r) => r + 90);
463
+ setTimeout(() => this.clampPosition());
464
+ this.cachedConstraints = null; // Invalidate cache on rotate
465
+ }
466
+ // Flip
467
+ flipHorizontal() {
468
+ this.flipH.update((f) => !f);
469
+ this.cachedConstraints = null; // Invalidate cache on flip
470
+ }
471
+ flipVertical() {
472
+ this.flipV.update((f) => !f);
473
+ this.cachedConstraints = null; // Invalidate cache on flip
474
+ }
475
+ reset() {
476
+ this.scale.set(1);
477
+ this.translateX.set(0);
478
+ this.translateY.set(0);
479
+ this.rotate.set(0);
480
+ this.flipH.set(false);
481
+ this.flipV.set(false);
482
+ this.cachedConstraints = null; // Invalidate cache on reset
483
+ }
484
+ next() {
485
+ const imgs = this.images();
486
+ if (!imgs)
487
+ return;
488
+ if (this.currentIndex() < imgs.length - 1) {
489
+ this.currentIndex.update((i) => i + 1);
490
+ }
491
+ }
492
+ prev() {
493
+ if (this.currentIndex() > 0) {
494
+ this.currentIndex.update((i) => i - 1);
495
+ }
496
+ }
497
+ // Mouse Interaction
498
+ onMouseDown(event) {
499
+ if (this.scale() <= 1 && !this.isDragging())
500
+ return; // Can drag only if zoomed? Or always? Default: only zoomed?
501
+ // User expectation for preview: maybe panning always allowed? or only when zoomed?
502
+ // Best practice: if image fits, no pan. If zoomed, pan.
503
+ // I'll allow pan if scale > 1
504
+ if (this.scale() <= 1)
505
+ return;
506
+ this.isDragging.set(true);
507
+ this.startX = event.clientX;
508
+ this.startY = event.clientY;
509
+ this.lastTranslateX = this.translateX();
510
+ this.lastTranslateY = this.translateY();
511
+ event.preventDefault();
512
+ }
513
+ // Touch Interaction (bound in template)
514
+ onTouchStart(event) {
515
+ this.stopInertia(); // Cancel any ongoing movement
516
+ const touches = event.touches;
517
+ if (touches.length === 1) {
518
+ // Single touch: Pan. Only if touching the image directly.
519
+ // We need to check if the target is the image element.
520
+ const imgElement = this.imgRef()?.nativeElement;
521
+ if (imgElement && event.target === imgElement) {
522
+ this.isDragging.set(true);
523
+ this.lastTouchX = touches[0].clientX;
524
+ this.lastTouchY = touches[0].clientY;
525
+ this.lastTimestamp = Date.now(); // Keep for scroll decay reference if needed
526
+ // Initialize physics state
527
+ this.velocityX = 0;
528
+ this.velocityY = 0;
529
+ this.touchHistory = [
530
+ {
531
+ x: touches[0].clientX,
532
+ y: touches[0].clientY,
533
+ time: Date.now(),
534
+ },
535
+ ];
536
+ // Cache layout to prevent thrashing
537
+ this.cachedConstraints = this.getConstraints();
538
+ }
539
+ }
540
+ else if (touches.length === 2) {
541
+ // Two fingers: Pinch
542
+ this.isPinching = true;
543
+ this.isDragging.set(false); // Stop panning
544
+ this.initialPinchDistance = this.getDistance(touches);
545
+ this.initialScale = this.scale();
546
+ // Clear caching on pinch (scale changes will invalidate limits)
547
+ this.cachedConstraints = null;
548
+ // Prevent default to avoid browser zoom
549
+ if (event.cancelable)
550
+ event.preventDefault();
551
+ }
552
+ }
553
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: ImagesPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
554
+ 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", first: true, predicate: ["imgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
555
+ <div
556
+ class="overlay"
557
+ [@fadeInOut]
558
+ (click)="close()"
559
+ (keydown)="onOverlayKey($event)"
560
+ tabindex="0"
561
+ role="button"
562
+ aria-label="Close preview overlay"
563
+ >
564
+ <!-- Custom Template Support -->
565
+ @if (customTemplate(); as template) {
566
+ <ng-container
567
+ *ngTemplateOutlet="template; context: { $implicit: state(), actions: actions }"
568
+ ></ng-container>
569
+ } @else {
570
+ <!-- Loading -->
571
+ @if (isLoading()) {
572
+ <div class="loader">Loading...</div>
573
+ }
574
+
575
+ <!-- Error -->
576
+ @if (hasError()) {
577
+ <div class="error">Failed to load image</div>
578
+ }
579
+
580
+ <!-- Image Container -->
581
+ <div
582
+ class="image-container"
583
+ (keydown)="onContainerKey($event)"
584
+ (touchstart)="onTouchStart($event)"
585
+ tabindex="-1"
586
+ >
587
+ <img
588
+ #imgRef
589
+ [src]="activeSrc()"
590
+ [class.opacity-0]="isLoading() || hasError()"
591
+ class="preview-image"
592
+ [class.dragging]="isDragging()"
593
+ [class.zoom-in]="scale() === 1"
594
+ [class.zoom-out]="scale() > 1"
595
+ [style.transform]="transformStyle()"
596
+ (load)="onImageLoad()"
597
+ (error)="onImageError()"
598
+ (mousedown)="onMouseDown($event)"
599
+ (click)="$event.stopPropagation()"
600
+ draggable="false"
601
+ alt="Preview"
602
+ />
603
+ </div>
604
+
605
+ <!-- Close Button -->
606
+ <button class="close-btn" (click)="close()" aria-label="Close preview">
607
+ <svg viewBox="0 0 24 24">
608
+ <path
609
+ 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"
610
+ />
611
+ </svg>
612
+ </button>
613
+
614
+ <!-- Navigation -->
615
+ @if (images() && images()!.length > 1) {
616
+ <!-- Check if not first -->
617
+ @if (currentIndex() > 0) {
618
+ <button
619
+ class="nav-btn prev"
620
+ (click)="prev(); $event.stopPropagation()"
621
+ (mousedown)="$event.stopPropagation()"
622
+ aria-label="Previous image"
623
+ >
624
+ <svg viewBox="0 0 24 24">
625
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
626
+ </svg>
627
+ </button>
628
+ }
629
+ <!-- Check if not last -->
630
+ @if (currentIndex() < images()!.length - 1) {
631
+ <button
632
+ class="nav-btn next"
633
+ (click)="next(); $event.stopPropagation()"
634
+ (mousedown)="$event.stopPropagation()"
635
+ aria-label="Next image"
636
+ >
637
+ <svg viewBox="0 0 24 24">
638
+ <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
639
+ </svg>
640
+ </button>
641
+ }
642
+
643
+ <!-- Counter -->
644
+ <div class="counter">{{ currentIndex() + 1 }} / {{ images()!.length }}</div>
645
+ }
646
+
647
+ <!-- Toolbar -->
648
+ <div
649
+ class="toolbar"
650
+ (click)="$event.stopPropagation()"
651
+ (keydown)="onToolbarKey($event)"
652
+ tabindex="0"
653
+ >
654
+ <!-- Flip H -->
655
+ <button class="toolbar-btn" (click)="flipHorizontal()" aria-label="Flip Horizontal">
656
+ <svg viewBox="0 0 24 24">
657
+ <path
658
+ 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"
659
+ />
660
+ </svg>
661
+ </button>
662
+ <!-- Flip V -->
663
+ <button class="toolbar-btn" (click)="flipVertical()" aria-label="Flip Vertical">
664
+ <svg viewBox="0 0 24 24">
665
+ <path
666
+ 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"
667
+ />
668
+ </svg>
669
+ </button>
670
+ <!-- Rotate Left -->
671
+ <button class="toolbar-btn" (click)="rotateLeft()" aria-label="Rotate Left">
672
+ <svg viewBox="0 0 24 24">
673
+ <path
674
+ 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"
675
+ />
676
+ </svg>
677
+ </button>
678
+ <!-- Rotate Right -->
679
+ <button class="toolbar-btn" (click)="rotateRight()" aria-label="Rotate Right">
680
+ <svg viewBox="0 0 24 24">
681
+ <path
682
+ 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"
683
+ />
684
+ </svg>
685
+ </button>
686
+ <!-- Zoom Out -->
687
+ <button class="toolbar-btn" (click)="zoomOut()" aria-label="Zoom Out">
688
+ <svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z" /></svg>
689
+ </button>
690
+ <!-- Zoom In -->
691
+ <button class="toolbar-btn" (click)="zoomIn()" aria-label="Zoom In">
692
+ <svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /></svg>
693
+ </button>
694
+ </div>
695
+ }
696
+ </div>
697
+ `, isInline: true, styles: [":host{display:block}.overlay{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background-color:#000000f2;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}.loader,.error{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fffc;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{width:auto;height:auto;max-width:100%;max-height:100%;margin:auto;object-fit:contain;pointer-events:auto;will-change:transform;transform-origin:center center;transition:transform .1s ease-out;touch-action:none}.preview-image.dragging{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:16px;background-color:#ffffff1a;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:10px 20px;border-radius:24px;pointer-events:auto;z-index:60}.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:#fff3}.toolbar-btn:focus-visible{outline:2px solid white;outline-offset:2px}.toolbar-btn svg{width:24px;height:24px;fill:currentColor}.close-btn{position:absolute;top:20px;right:20px;z-index:60;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 svg{width:24px;height:24px;fill:currentColor}.close-btn:hover{background:#fff3;transform:rotate(90deg)}.nav-btn{position:absolute;top:50%;transform:translateY(-50%);width:48px;height:48px;border-radius:50%;background:#ffffff1a;border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;z-index:10}.nav-btn:hover{background:#fff3}.nav-btn.prev{left:20px}.nav-btn.next{right:20px}.nav-btn 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:#fffc;font-size:14px;background:#0000004d;padding:4px 12px;border-radius:12px;pointer-events:none;z-index:10}.hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], animations: [
698
+ trigger('fadeInOut', [
699
+ transition(':enter', [
700
+ style({ opacity: 0 }),
701
+ animate('200ms ease-out', style({ opacity: 1 })),
702
+ ]),
703
+ transition(':leave', [animate('200ms ease-in', style({ opacity: 0 }))]),
704
+ ]),
705
+ ], changeDetection: i0.ChangeDetectionStrategy.OnPush });
706
+ }
707
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: ImagesPreviewComponent, decorators: [{
708
+ type: Component,
709
+ args: [{ selector: 'ng-images-preview', standalone: true, imports: [CommonModule], template: `
710
+ <div
711
+ class="overlay"
712
+ [@fadeInOut]
713
+ (click)="close()"
714
+ (keydown)="onOverlayKey($event)"
715
+ tabindex="0"
716
+ role="button"
717
+ aria-label="Close preview overlay"
718
+ >
719
+ <!-- Custom Template Support -->
720
+ @if (customTemplate(); as template) {
721
+ <ng-container
722
+ *ngTemplateOutlet="template; context: { $implicit: state(), actions: actions }"
723
+ ></ng-container>
724
+ } @else {
725
+ <!-- Loading -->
726
+ @if (isLoading()) {
727
+ <div class="loader">Loading...</div>
728
+ }
729
+
730
+ <!-- Error -->
731
+ @if (hasError()) {
732
+ <div class="error">Failed to load image</div>
733
+ }
734
+
735
+ <!-- Image Container -->
736
+ <div
737
+ class="image-container"
738
+ (keydown)="onContainerKey($event)"
739
+ (touchstart)="onTouchStart($event)"
740
+ tabindex="-1"
741
+ >
742
+ <img
743
+ #imgRef
744
+ [src]="activeSrc()"
745
+ [class.opacity-0]="isLoading() || hasError()"
746
+ class="preview-image"
747
+ [class.dragging]="isDragging()"
748
+ [class.zoom-in]="scale() === 1"
749
+ [class.zoom-out]="scale() > 1"
750
+ [style.transform]="transformStyle()"
751
+ (load)="onImageLoad()"
752
+ (error)="onImageError()"
753
+ (mousedown)="onMouseDown($event)"
754
+ (click)="$event.stopPropagation()"
755
+ draggable="false"
756
+ alt="Preview"
757
+ />
758
+ </div>
759
+
760
+ <!-- Close Button -->
761
+ <button class="close-btn" (click)="close()" aria-label="Close preview">
762
+ <svg viewBox="0 0 24 24">
763
+ <path
764
+ 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"
765
+ />
766
+ </svg>
767
+ </button>
768
+
769
+ <!-- Navigation -->
770
+ @if (images() && images()!.length > 1) {
771
+ <!-- Check if not first -->
772
+ @if (currentIndex() > 0) {
773
+ <button
774
+ class="nav-btn prev"
775
+ (click)="prev(); $event.stopPropagation()"
776
+ (mousedown)="$event.stopPropagation()"
777
+ aria-label="Previous image"
778
+ >
779
+ <svg viewBox="0 0 24 24">
780
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
781
+ </svg>
782
+ </button>
783
+ }
784
+ <!-- Check if not last -->
785
+ @if (currentIndex() < images()!.length - 1) {
786
+ <button
787
+ class="nav-btn next"
788
+ (click)="next(); $event.stopPropagation()"
789
+ (mousedown)="$event.stopPropagation()"
790
+ aria-label="Next image"
791
+ >
792
+ <svg viewBox="0 0 24 24">
793
+ <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
794
+ </svg>
795
+ </button>
796
+ }
797
+
798
+ <!-- Counter -->
799
+ <div class="counter">{{ currentIndex() + 1 }} / {{ images()!.length }}</div>
800
+ }
801
+
802
+ <!-- Toolbar -->
803
+ <div
804
+ class="toolbar"
805
+ (click)="$event.stopPropagation()"
806
+ (keydown)="onToolbarKey($event)"
807
+ tabindex="0"
808
+ >
809
+ <!-- Flip H -->
810
+ <button class="toolbar-btn" (click)="flipHorizontal()" aria-label="Flip Horizontal">
811
+ <svg viewBox="0 0 24 24">
812
+ <path
813
+ 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"
814
+ />
815
+ </svg>
816
+ </button>
817
+ <!-- Flip V -->
818
+ <button class="toolbar-btn" (click)="flipVertical()" aria-label="Flip Vertical">
819
+ <svg viewBox="0 0 24 24">
820
+ <path
821
+ 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"
822
+ />
823
+ </svg>
824
+ </button>
825
+ <!-- Rotate Left -->
826
+ <button class="toolbar-btn" (click)="rotateLeft()" aria-label="Rotate Left">
827
+ <svg viewBox="0 0 24 24">
828
+ <path
829
+ 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"
830
+ />
831
+ </svg>
832
+ </button>
833
+ <!-- Rotate Right -->
834
+ <button class="toolbar-btn" (click)="rotateRight()" aria-label="Rotate Right">
835
+ <svg viewBox="0 0 24 24">
836
+ <path
837
+ 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"
838
+ />
839
+ </svg>
840
+ </button>
841
+ <!-- Zoom Out -->
842
+ <button class="toolbar-btn" (click)="zoomOut()" aria-label="Zoom Out">
843
+ <svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z" /></svg>
844
+ </button>
845
+ <!-- Zoom In -->
846
+ <button class="toolbar-btn" (click)="zoomIn()" aria-label="Zoom In">
847
+ <svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /></svg>
848
+ </button>
849
+ </div>
850
+ }
851
+ </div>
852
+ `, animations: [
853
+ trigger('fadeInOut', [
854
+ transition(':enter', [
855
+ style({ opacity: 0 }),
856
+ animate('200ms ease-out', style({ opacity: 1 })),
857
+ ]),
858
+ transition(':leave', [animate('200ms ease-in', style({ opacity: 0 }))]),
859
+ ]),
860
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
861
+ '(document:mouseup)': 'onMouseUp()',
862
+ '(document:touchmove)': 'onTouchMove($event)',
863
+ '(document:touchend)': 'onTouchEnd($event)',
864
+ '(document:keydown.arrowleft)': 'prev()',
865
+ '(document:keydown.arrowright)': 'next()',
866
+ '(document:keydown.escape)': 'close()',
867
+ }, styles: [":host{display:block}.overlay{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background-color:#000000f2;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}.loader,.error{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fffc;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{width:auto;height:auto;max-width:100%;max-height:100%;margin:auto;object-fit:contain;pointer-events:auto;will-change:transform;transform-origin:center center;transition:transform .1s ease-out;touch-action:none}.preview-image.dragging{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:16px;background-color:#ffffff1a;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:10px 20px;border-radius:24px;pointer-events:auto;z-index:60}.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:#fff3}.toolbar-btn:focus-visible{outline:2px solid white;outline-offset:2px}.toolbar-btn svg{width:24px;height:24px;fill:currentColor}.close-btn{position:absolute;top:20px;right:20px;z-index:60;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 svg{width:24px;height:24px;fill:currentColor}.close-btn:hover{background:#fff3;transform:rotate(90deg)}.nav-btn{position:absolute;top:50%;transform:translateY(-50%);width:48px;height:48px;border-radius:50%;background:#ffffff1a;border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;z-index:10}.nav-btn:hover{background:#fff3}.nav-btn.prev{left:20px}.nav-btn.next{right:20px}.nav-btn 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:#fffc;font-size:14px;background:#0000004d;padding:4px 12px;border-radius:12px;pointer-events:none;z-index:10}.hidden{display:none}\n"] }]
868
+ }], 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 }] }], imgRef: [{ type: i0.ViewChild, args: ['imgRef', { isSignal: true }] }], onEscape: [{
869
+ type: HostListener,
870
+ args: ['document:keydown.escape']
871
+ }], onMouseMove: [{
872
+ type: HostListener,
873
+ args: ['document:mousemove', ['$event']]
874
+ }] } });
875
+
876
+ class ImagesPreviewDirective {
877
+ highResSrc = '';
878
+ previewImages = [];
879
+ previewTemplate;
880
+ componentRef = null;
881
+ appRef = inject(ApplicationRef);
882
+ injector = inject(EnvironmentInjector);
883
+ el = inject((ElementRef));
884
+ onClick(event) {
885
+ event.stopPropagation();
886
+ // Prevent duplicate open
887
+ if (this.componentRef)
888
+ return;
889
+ // Determine Source
890
+ const hostEl = this.el.nativeElement;
891
+ let src = this.highResSrc || hostEl.getAttribute('src') || hostEl.src;
892
+ // If no src found on host, try to find an img child
893
+ if (!src) {
894
+ const imgChild = hostEl.querySelector('img');
895
+ if (imgChild) {
896
+ src = imgChild.getAttribute('src') || imgChild.src;
897
+ }
898
+ }
899
+ src = src || '';
900
+ if (src) {
901
+ this.openPreview(src);
902
+ }
903
+ }
904
+ cursor = 'pointer';
905
+ openPreview(src) {
906
+ // Create Component
907
+ this.componentRef = createComponent(ImagesPreviewComponent, {
908
+ environmentInjector: this.injector
909
+ });
910
+ // Set Inputs
911
+ this.componentRef.setInput('src', src);
912
+ if (this.previewImages.length > 0) {
913
+ this.componentRef.setInput('images', this.previewImages);
914
+ const index = this.previewImages.indexOf(src);
915
+ this.componentRef.setInput('initialIndex', index >= 0 ? index : 0);
916
+ }
917
+ if (this.previewTemplate) {
918
+ this.componentRef.setInput('customTemplate', this.previewTemplate);
919
+ }
920
+ // Set Callbacks
921
+ this.componentRef.instance.closeCallback = () => this.destroyPreview();
922
+ // Attach to App
923
+ this.appRef.attachView(this.componentRef.hostView);
924
+ // Append to Body
925
+ const domElem = this.componentRef.hostView.rootNodes[0];
926
+ document.body.appendChild(domElem);
927
+ }
928
+ destroyPreview() {
929
+ if (this.componentRef) {
930
+ this.appRef.detachView(this.componentRef.hostView);
931
+ this.componentRef.destroy();
932
+ this.componentRef = null;
933
+ }
934
+ }
935
+ ngOnDestroy() {
936
+ this.destroyPreview();
937
+ }
938
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: ImagesPreviewDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
939
+ 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 });
940
+ }
941
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.7", ngImport: i0, type: ImagesPreviewDirective, decorators: [{
942
+ type: Directive,
943
+ args: [{
944
+ selector: '[ngImagesPreview]',
945
+ standalone: true
946
+ }]
947
+ }], propDecorators: { highResSrc: [{
948
+ type: Input,
949
+ args: ['ngImagesPreview']
950
+ }], previewImages: [{
951
+ type: Input
952
+ }], previewTemplate: [{
953
+ type: Input
954
+ }], onClick: [{
955
+ type: HostListener,
956
+ args: ['click', ['$event']]
957
+ }], cursor: [{
958
+ type: HostListener,
959
+ args: ['style.cursor']
960
+ }] } });
961
+
962
+ /*
963
+ * Public API Surface of ng-images-preview
964
+ */
965
+
966
+ /**
967
+ * Generated bundle index. Do not edit.
968
+ */
969
+
970
+ export { ImagesPreviewComponent, ImagesPreviewDirective };
971
+ //# sourceMappingURL=ng-images-preview.mjs.map