@sumaris-net/ngx-components 18.25.5 → 18.25.7

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.
@@ -28334,6 +28334,676 @@ ProgressInterceptor = __decorate([
28334
28334
  __param(0, Inject(APP_PROGRESS_BAR_SERVICE))
28335
28335
  ], ProgressInterceptor);
28336
28336
 
28337
+ /**
28338
+ * The zoom/pan/rotate/crop slideshow shown when clicking an image in `AppImageGalleryComponent`.
28339
+ * Extracted into its own component so the exact same slideshow can be presented either inside an
28340
+ * `ion-modal` (default), or inline as an overlay directly within the gallery, without any duplication.
28341
+ */
28342
+ class AppImageGallerySlideshowComponent {
28343
+ alterCtrl;
28344
+ translate;
28345
+ cd;
28346
+ activeSlideIndex = 0;
28347
+ swiperModules = [IonicSlides];
28348
+ _swiper;
28349
+ // Mouse wheel zoom & rotation state
28350
+ _zoomState = { scale: 1, tx: 0, ty: 0 };
28351
+ _rotation = 0; // degrees: 0, 90, 180, 270
28352
+ _dragState = null;
28353
+ _wheelListener = null;
28354
+ _wheelEl = null;
28355
+ // Crop & rotate state. Rotation is always available; the crop grid/handles only show once the
28356
+ // crop tool has been explicitly activated (fab button). Validate/cancel show up once something changed.
28357
+ _cropToolActive = false;
28358
+ _cropRect = { x: 0, y: 0, width: 1, height: 1 };
28359
+ _cropOverlayRect = null;
28360
+ _cropDragState = null;
28361
+ _cropHandles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
28362
+ rows = [];
28363
+ selectedIndex = 0;
28364
+ mobile = false;
28365
+ readOnly = false;
28366
+ disabled = false;
28367
+ debug = false;
28368
+ mouseZoomEnabled = false;
28369
+ wheelZoomStep = 0.3;
28370
+ maxZoomRatio = 3;
28371
+ rotationEnabled = true;
28372
+ cropEnabled = true;
28373
+ imageExportFormat = 'png';
28374
+ imageExportQuality = 0.92;
28375
+ imageSizeQueryParam = 'size';
28376
+ modalImageSize;
28377
+ /** Requests deletion of the given row. The caller (gallery) owns the confirmation/deletion logic and resolves the promise with whether it actually happened. */
28378
+ deleteRequested = createPromiseEventEmitter();
28379
+ closeRequested = new EventEmitter();
28380
+ afterEditImage = new EventEmitter();
28381
+ afterEditRow = new EventEmitter();
28382
+ swiperRef;
28383
+ showTooltip = inject(APP_SHOW_TOOLTIP, { optional: true });
28384
+ get hasPendingEdit() {
28385
+ const r = this._cropRect;
28386
+ return this._rotation !== 0 || r.x !== 0 || r.y !== 0 || r.width !== 1 || r.height !== 1;
28387
+ }
28388
+ constructor(alterCtrl, translate, cd) {
28389
+ this.alterCtrl = alterCtrl;
28390
+ this.translate = translate;
28391
+ this.cd = cd;
28392
+ }
28393
+ ngOnDestroy() {
28394
+ this._cleanupWheelZoom();
28395
+ }
28396
+ getSwiper(swiperElement) {
28397
+ this._swiper = (swiperElement || this.swiperRef?.nativeElement)?.swiper || this._swiper;
28398
+ return this._swiper;
28399
+ }
28400
+ onSlideChange(swiperElement) {
28401
+ const swiper = this.getSwiper(swiperElement);
28402
+ this.activeSlideIndex = swiper.activeIndex;
28403
+ this._resetZoom(swiperElement);
28404
+ this._updateSwiperTouchMove(swiperElement);
28405
+ this.markForCheck();
28406
+ }
28407
+ onSwiperInit(swiperElement) {
28408
+ this._resetZoom();
28409
+ if (this.selectedIndex > 0) {
28410
+ if (this.debug)
28411
+ console.debug(`[image-gallery-slideshow] Initializing slideshow, selectedIndex=${this.selectedIndex}`);
28412
+ // Use a setTimeout otherwise the swiper cannot slide
28413
+ setTimeout(() => {
28414
+ const swiper = this.getSwiper(swiperElement);
28415
+ swiper.slideTo(this.selectedIndex, 0);
28416
+ });
28417
+ }
28418
+ this._setupWheelZoom(swiperElement);
28419
+ this._updateSwiperTouchMove(swiperElement);
28420
+ setTimeout(() => {
28421
+ this._updateEditImageSize(swiperElement);
28422
+ this._updateCropOverlayPosition(swiperElement);
28423
+ });
28424
+ }
28425
+ /**
28426
+ * Swiper's own touch/mouse-drag-to-swipe gesture is captured very early (before our crop overlay's
28427
+ * own mousedown handler can stop it), so it must be disabled upfront whenever the crop tool is active
28428
+ * -- otherwise dragging inside the crop selection ends up sliding to the next/previous image instead.
28429
+ * Prev/next arrow buttons keep working, since they call the Swiper API directly.
28430
+ */
28431
+ _updateSwiperTouchMove(swiperElement) {
28432
+ const swiper = this.getSwiper(swiperElement);
28433
+ if (!swiper)
28434
+ return;
28435
+ swiper.allowTouchMove = !this._cropToolActive;
28436
+ }
28437
+ async zoom(swiperElement, zoomIn) {
28438
+ if (this.debug)
28439
+ console.debug('[image-gallery-slideshow] zoom ' + (zoomIn ? 'in' : 'out'), swiperElement);
28440
+ if (this.mouseZoomEnabled) {
28441
+ const delta = zoomIn ? this.wheelZoomStep : -this.wheelZoomStep;
28442
+ const newScale = Math.max(1, Math.min(this._zoomState.scale + delta, this.maxZoomRatio));
28443
+ if (newScale === this._zoomState.scale)
28444
+ return;
28445
+ if (newScale === 1) {
28446
+ this._zoomState = { scale: 1, tx: 0, ty: 0 };
28447
+ }
28448
+ else {
28449
+ this._zoomState.scale = newScale;
28450
+ this._clampTranslations(swiperElement);
28451
+ }
28452
+ this._applyZoom(swiperElement);
28453
+ }
28454
+ else {
28455
+ const swiper = this.getSwiper(swiperElement);
28456
+ if (zoomIn)
28457
+ swiper.zoom.in();
28458
+ else
28459
+ swiper.zoom.out();
28460
+ }
28461
+ }
28462
+ async requestDelete(swiperElement) {
28463
+ const swiper = this.getSwiper(swiperElement);
28464
+ const currentRow = this.rows.find((row, index) => index === swiper.activeIndex);
28465
+ if (!currentRow)
28466
+ return false; // Row not found
28467
+ let deleted = false;
28468
+ try {
28469
+ deleted = await emitPromiseEvent(this.deleteRequested, 'delete', { detail: { row: currentRow } });
28470
+ }
28471
+ catch (err) {
28472
+ if (err === 'CANCELLED')
28473
+ return false; // User cancel
28474
+ throw err;
28475
+ }
28476
+ if (deleted) {
28477
+ if (!this.rows?.length) {
28478
+ this.closeRequested.emit();
28479
+ }
28480
+ else {
28481
+ this._resetZoom(swiperElement);
28482
+ swiper.update();
28483
+ this.markForCheck();
28484
+ }
28485
+ }
28486
+ return deleted;
28487
+ }
28488
+ async slidePrev(swiperElement) {
28489
+ const swiper = this.getSwiper(swiperElement);
28490
+ const activeIndex = swiper.activeIndex;
28491
+ if (activeIndex === 0) {
28492
+ // Loop to last
28493
+ await swiper.slideTo(this.rows.length - 1, 0 /*no transition, to avoid a "revert" effect*/);
28494
+ }
28495
+ else {
28496
+ await swiper.slidePrev();
28497
+ }
28498
+ }
28499
+ async slideNext(swiperElement) {
28500
+ const swiper = this.getSwiper(swiperElement);
28501
+ const activeIndex = swiper.activeIndex;
28502
+ if (activeIndex === this.rows.length - 1) {
28503
+ // Loop to first
28504
+ await swiper.slideTo(0, 0 /*no transition, to avoid a "revert" effect*/);
28505
+ }
28506
+ else {
28507
+ await swiper.slideNext();
28508
+ }
28509
+ }
28510
+ getImageSizeQueryParams() {
28511
+ if (!this.imageSizeQueryParam || isNilOrBlank(this.modalImageSize))
28512
+ return undefined;
28513
+ return {
28514
+ [this.imageSizeQueryParam]: this.modalImageSize,
28515
+ };
28516
+ }
28517
+ // --- Crop & rotate tools: always available (no "enter edit mode" step) ---
28518
+ // Validate/Cancel only appear once the user actually changed something (see `hasPendingEdit`).
28519
+ discardEdit(swiperElement) {
28520
+ this._resetEditState(swiperElement);
28521
+ }
28522
+ resetCrop(_swiperElement) {
28523
+ this._cropRect = { x: 0, y: 0, width: 1, height: 1 };
28524
+ this.markForCheck();
28525
+ }
28526
+ toggleCropTool(swiperElement) {
28527
+ if (!this.cropEnabled || this.readOnly)
28528
+ return;
28529
+ this._cropToolActive = !this._cropToolActive;
28530
+ // Discard any pending crop selection when hiding the tool
28531
+ if (!this._cropToolActive) {
28532
+ this._cropRect = { x: 0, y: 0, width: 1, height: 1 };
28533
+ this._cropDragState = null;
28534
+ }
28535
+ this._updateSwiperTouchMove(swiperElement);
28536
+ this.markForCheck();
28537
+ if (this._cropToolActive) {
28538
+ // Wait for the crop overlay to be rendered, then compute its position
28539
+ setTimeout(() => this._updateCropOverlayPosition(swiperElement));
28540
+ }
28541
+ }
28542
+ async validateEdit(swiperElement) {
28543
+ const swiper = this.getSwiper(swiperElement);
28544
+ const currentRow = this.rows.find((row, index) => index === swiper.activeIndex);
28545
+ if (!currentRow)
28546
+ return;
28547
+ const image = currentRow.currentData;
28548
+ const format = this.imageExportFormat;
28549
+ const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png';
28550
+ const quality = format === 'jpeg' ? this.imageExportQuality : undefined;
28551
+ try {
28552
+ const canvas = await this._renderEditedImage(image, this._rotation, this._cropRect);
28553
+ const dataUrl = canvas.toDataURL(mimeType, quality);
28554
+ const blob = await new Promise((resolve, reject) => {
28555
+ canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('canvas.toBlob() failed'))), mimeType, quality);
28556
+ });
28557
+ // Update the row, so the slideshow and thumbnail reflect the change immediately
28558
+ image.dataUrl = dataUrl;
28559
+ image.url = null;
28560
+ this.afterEditImage.emit({ row: currentRow, image, dataUrl, blob, format });
28561
+ this.afterEditRow.emit(currentRow);
28562
+ this._resetEditState(swiperElement);
28563
+ }
28564
+ catch (err) {
28565
+ console.error('[image-gallery-slideshow] Failed to export the edited image', err);
28566
+ await Alerts.showError('IMAGE.GALLERY.ERROR_EDIT_IMAGE', this.alterCtrl, this.translate);
28567
+ }
28568
+ }
28569
+ _resetEditState(swiperElement) {
28570
+ this._rotation = 0;
28571
+ this._cropRect = { x: 0, y: 0, width: 1, height: 1 };
28572
+ this._cropDragState = null;
28573
+ this._cropToolActive = false;
28574
+ this._applyZoom(swiperElement);
28575
+ this._updateEditImageSize(swiperElement);
28576
+ this._updateSwiperTouchMove(swiperElement);
28577
+ this.markForCheck();
28578
+ }
28579
+ onWindowResize() {
28580
+ const swiperElement = this.swiperRef?.nativeElement;
28581
+ if (!swiperElement)
28582
+ return;
28583
+ this._updateEditImageSize(swiperElement);
28584
+ this._updateCropOverlayPosition(swiperElement);
28585
+ }
28586
+ rotate(swiperElement, direction) {
28587
+ if (!this.rotationEnabled)
28588
+ return;
28589
+ const delta = direction === 'right' ? 90 : -90;
28590
+ this._rotation = (((this._rotation + delta) % 360) + 360) % 360;
28591
+ this._applyZoom(swiperElement);
28592
+ this._updateEditImageSize(swiperElement);
28593
+ this._updateCropOverlayPosition(swiperElement);
28594
+ }
28595
+ /**
28596
+ * Constrains the displayed image so that, even once rotated 90°/270° via CSS transform,
28597
+ * its visual bounding box always fits inside the slide container (no overflow).
28598
+ */
28599
+ _updateEditImageSize(swiperElement, retriesLeft = 60) {
28600
+ const swiper = this.getSwiper(swiperElement);
28601
+ const activeSlide = swiper?.slides?.[swiper.activeIndex];
28602
+ if (!activeSlide)
28603
+ return;
28604
+ const containerEl = activeSlide.querySelector('.swiper-zoom-container');
28605
+ const imgEl = activeSlide.querySelector('.swiper-zoom-target, .swiper-zoom-container > img');
28606
+ if (!containerEl || !imgEl)
28607
+ return;
28608
+ if (!this.cropEnabled && !this.rotationEnabled) {
28609
+ imgEl.style.width = '';
28610
+ imgEl.style.height = '';
28611
+ return;
28612
+ }
28613
+ const containerRect = containerEl.getBoundingClientRect();
28614
+ const naturalSize = this._getImageNaturalSize(imgEl);
28615
+ // The container/image may not be laid out or loaded yet: skip and retry on the next frame
28616
+ if (!containerRect.width || !containerRect.height || !naturalSize) {
28617
+ if (retriesLeft > 0) {
28618
+ requestAnimationFrame(() => this._updateEditImageSize(swiperElement, retriesLeft - 1));
28619
+ }
28620
+ return;
28621
+ }
28622
+ // Compute the exact box (preserving the image's aspect ratio) that fits the available space,
28623
+ // swapping width/height when rotated 90°/270° so the rotated visual box never overflows.
28624
+ const swapped = this._rotation % 180 !== 0;
28625
+ const availWidth = swapped ? containerRect.height : containerRect.width;
28626
+ const availHeight = swapped ? containerRect.width : containerRect.height;
28627
+ const ratio = Math.min(availWidth / naturalSize.width, availHeight / naturalSize.height);
28628
+ imgEl.style.width = `${naturalSize.width * ratio}px`;
28629
+ imgEl.style.height = `${naturalSize.height * ratio}px`;
28630
+ }
28631
+ _getImageNaturalSize(imgEl) {
28632
+ const inner = (imgEl.tagName === 'ION-IMG' ? imgEl.shadowRoot?.querySelector('img') : imgEl);
28633
+ if (!inner?.naturalWidth || !inner?.naturalHeight)
28634
+ return null;
28635
+ return { width: inner.naturalWidth, height: inner.naturalHeight };
28636
+ }
28637
+ // --- Crop overlay: drag to move/resize the crop rectangle ---
28638
+ onCropPointerDown(event, swiperElement, handle) {
28639
+ event.preventDefault();
28640
+ event.stopPropagation();
28641
+ const pt = this._getEventPoint(event);
28642
+ this._cropOverlayRect = this._cropOverlayRect ?? this._updateCropOverlayPosition(swiperElement);
28643
+ this._cropDragState = {
28644
+ handle,
28645
+ startX: pt.x,
28646
+ startY: pt.y,
28647
+ startRect: { ...this._cropRect },
28648
+ };
28649
+ // Also disable programmatic slide navigation while dragging (allowTouchMove is already
28650
+ // permanently off whenever crop is enabled, see `_updateSwiperTouchMove`)
28651
+ const swiper = this.getSwiper(swiperElement);
28652
+ if (swiper) {
28653
+ swiper.allowSlideNext = false;
28654
+ swiper.allowSlidePrev = false;
28655
+ }
28656
+ }
28657
+ onCropPointerMove(event, _swiperElement) {
28658
+ if (!this._cropDragState)
28659
+ return;
28660
+ event.preventDefault();
28661
+ const rect = this._cropOverlayRect;
28662
+ if (!rect?.width || !rect?.height)
28663
+ return;
28664
+ const pt = this._getEventPoint(event);
28665
+ const dx = (pt.x - this._cropDragState.startX) / rect.width;
28666
+ const dy = (pt.y - this._cropDragState.startY) / rect.height;
28667
+ this._cropRect = this._computeCropRect(this._cropDragState.handle, this._cropDragState.startRect, dx, dy);
28668
+ this.markForCheck();
28669
+ }
28670
+ onCropPointerUp(_event, swiperElement) {
28671
+ if (!this._cropDragState)
28672
+ return;
28673
+ this._cropDragState = null;
28674
+ const swiper = swiperElement ? this.getSwiper(swiperElement) : null;
28675
+ if (swiper) {
28676
+ swiper.allowSlideNext = true;
28677
+ swiper.allowSlidePrev = true;
28678
+ }
28679
+ if (swiperElement)
28680
+ this._updateSwiperTouchMove(swiperElement);
28681
+ }
28682
+ _computeCropRect(handle, start, dx, dy) {
28683
+ const MIN_SIZE = 0.08;
28684
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
28685
+ if (handle === 'move') {
28686
+ let x = clamp01(start.x + dx);
28687
+ let y = clamp01(start.y + dy);
28688
+ x = Math.min(x, 1 - start.width);
28689
+ y = Math.min(y, 1 - start.height);
28690
+ return { x, y, width: start.width, height: start.height };
28691
+ }
28692
+ let left = start.x;
28693
+ let top = start.y;
28694
+ let right = start.x + start.width;
28695
+ let bottom = start.y + start.height;
28696
+ if (handle.includes('w'))
28697
+ left = clamp01(start.x + dx);
28698
+ if (handle.includes('e'))
28699
+ right = clamp01(start.x + start.width + dx);
28700
+ if (handle.includes('n'))
28701
+ top = clamp01(start.y + dy);
28702
+ if (handle.includes('s'))
28703
+ bottom = clamp01(start.y + start.height + dy);
28704
+ if (right - left < MIN_SIZE) {
28705
+ if (handle.includes('w'))
28706
+ left = right - MIN_SIZE;
28707
+ else
28708
+ right = left + MIN_SIZE;
28709
+ }
28710
+ if (bottom - top < MIN_SIZE) {
28711
+ if (handle.includes('n'))
28712
+ top = bottom - MIN_SIZE;
28713
+ else
28714
+ bottom = top + MIN_SIZE;
28715
+ }
28716
+ left = clamp01(left);
28717
+ right = clamp01(right);
28718
+ top = clamp01(top);
28719
+ bottom = clamp01(bottom);
28720
+ return { x: left, y: top, width: right - left, height: bottom - top };
28721
+ }
28722
+ _getEventPoint(event) {
28723
+ if ('touches' in event && event.touches.length) {
28724
+ return { x: event.touches[0].clientX, y: event.touches[0].clientY };
28725
+ }
28726
+ if ('changedTouches' in event && event.changedTouches.length) {
28727
+ return { x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY };
28728
+ }
28729
+ const mouseEvent = event;
28730
+ return { x: mouseEvent.clientX, y: mouseEvent.clientY };
28731
+ }
28732
+ _updateCropOverlayPosition(swiperElement, retriesLeft = 60) {
28733
+ const swiper = this.getSwiper(swiperElement);
28734
+ const activeSlide = swiper?.slides?.[swiper.activeIndex];
28735
+ if (!activeSlide)
28736
+ return null;
28737
+ const containerEl = activeSlide.querySelector('.swiper-zoom-container');
28738
+ const imgEl = activeSlide.querySelector('.swiper-zoom-target, .swiper-zoom-container > img');
28739
+ const overlayEl = activeSlide.querySelector('.crop-overlay');
28740
+ if (!containerEl || !imgEl || !overlayEl)
28741
+ return null;
28742
+ const containerRect = containerEl.getBoundingClientRect();
28743
+ const imgRect = imgEl.getBoundingClientRect();
28744
+ // The image may not be laid out yet: keep any previous (non-zero) value and retry on the next frame
28745
+ if (!imgRect.width || !imgRect.height) {
28746
+ if (retriesLeft > 0) {
28747
+ requestAnimationFrame(() => this._updateCropOverlayPosition(swiperElement, retriesLeft - 1));
28748
+ }
28749
+ return this._cropOverlayRect;
28750
+ }
28751
+ overlayEl.style.left = `${imgRect.left - containerRect.left}px`;
28752
+ overlayEl.style.top = `${imgRect.top - containerRect.top}px`;
28753
+ overlayEl.style.width = `${imgRect.width}px`;
28754
+ overlayEl.style.height = `${imgRect.height}px`;
28755
+ this._cropOverlayRect = { width: imgRect.width, height: imgRect.height };
28756
+ return this._cropOverlayRect;
28757
+ }
28758
+ // --- Canvas rendering, used to export the cropped/rotated image ---
28759
+ async _renderEditedImage(image, rotation, cropRect) {
28760
+ const src = image.dataUrl || image.url;
28761
+ const img = await this._loadImageElement(src);
28762
+ const sw = img.naturalWidth;
28763
+ const sh = img.naturalHeight;
28764
+ const swapped = rotation % 180 !== 0;
28765
+ const rw = swapped ? sh : sw;
28766
+ const rh = swapped ? sw : sh;
28767
+ // 1) Draw the full (uncropped) image, rotated
28768
+ const rotCanvas = document.createElement('canvas');
28769
+ rotCanvas.width = rw;
28770
+ rotCanvas.height = rh;
28771
+ const rctx = rotCanvas.getContext('2d');
28772
+ rctx.translate(rw / 2, rh / 2);
28773
+ rctx.rotate((rotation * Math.PI) / 180);
28774
+ rctx.drawImage(img, -sw / 2, -sh / 2);
28775
+ // 2) Crop the rotated image
28776
+ const cropX = Math.round(cropRect.x * rw);
28777
+ const cropY = Math.round(cropRect.y * rh);
28778
+ const cropW = Math.max(1, Math.round(cropRect.width * rw));
28779
+ const cropH = Math.max(1, Math.round(cropRect.height * rh));
28780
+ const outCanvas = document.createElement('canvas');
28781
+ outCanvas.width = cropW;
28782
+ outCanvas.height = cropH;
28783
+ outCanvas.getContext('2d').drawImage(rotCanvas, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);
28784
+ return outCanvas;
28785
+ }
28786
+ _loadImageElement(src) {
28787
+ return new Promise((resolve, reject) => {
28788
+ const img = new Image();
28789
+ if (!src.startsWith('data:'))
28790
+ img.crossOrigin = 'anonymous';
28791
+ img.onload = () => resolve(img);
28792
+ img.onerror = () => reject(new Error(`[image-gallery-slideshow] Cannot load image: ${src}`));
28793
+ img.src = src;
28794
+ });
28795
+ }
28796
+ // --- Mouse-wheel zoom & mouse-drag pan ---
28797
+ onPointerDown(event, swiperElement) {
28798
+ if (!this.mouseZoomEnabled || this._zoomState.scale <= 1)
28799
+ return;
28800
+ // Don't intercept clicks on buttons/controls
28801
+ if (event.target.closest('ion-fab-button, ion-button, button'))
28802
+ return;
28803
+ event.preventDefault();
28804
+ this._dragState = {
28805
+ startX: event.clientX,
28806
+ startY: event.clientY,
28807
+ startTx: this._zoomState.tx,
28808
+ startTy: this._zoomState.ty,
28809
+ };
28810
+ const wrapEl = this._getActiveZoomWrapEl(swiperElement);
28811
+ if (wrapEl)
28812
+ wrapEl.style.cursor = 'grabbing';
28813
+ // Disable Swiper slide-change while dragging
28814
+ const swiper = this.getSwiper(swiperElement);
28815
+ if (swiper) {
28816
+ swiper.allowSlideNext = false;
28817
+ swiper.allowSlidePrev = false;
28818
+ }
28819
+ }
28820
+ onPointerMove(event, swiperElement) {
28821
+ if (this._cropDragState) {
28822
+ this.onCropPointerMove(event, swiperElement);
28823
+ return;
28824
+ }
28825
+ if (!this._dragState)
28826
+ return;
28827
+ event.preventDefault();
28828
+ this._zoomState.tx = this._dragState.startTx + event.clientX - this._dragState.startX;
28829
+ this._zoomState.ty = this._dragState.startTy + event.clientY - this._dragState.startY;
28830
+ this._clampTranslations(swiperElement);
28831
+ this._applyZoom(swiperElement);
28832
+ }
28833
+ onPointerUp(event, swiperElement) {
28834
+ if (this._cropDragState) {
28835
+ this.onCropPointerUp(event, swiperElement);
28836
+ return;
28837
+ }
28838
+ if (!this._dragState)
28839
+ return;
28840
+ this._dragState = null;
28841
+ const wrapEl = this._getActiveZoomWrapEl(swiperElement);
28842
+ if (wrapEl)
28843
+ wrapEl.style.cursor = this._zoomState.scale > 1 ? 'grab' : '';
28844
+ // Re-enable Swiper navigation
28845
+ const swiper = swiperElement ? this.getSwiper(swiperElement) : null;
28846
+ if (swiper) {
28847
+ swiper.allowSlideNext = true;
28848
+ swiper.allowSlidePrev = true;
28849
+ }
28850
+ }
28851
+ _resetZoom(swiperElement) {
28852
+ this._zoomState = { scale: 1, tx: 0, ty: 0 };
28853
+ this._dragState = null;
28854
+ this._rotation = 0;
28855
+ this._cropRect = { x: 0, y: 0, width: 1, height: 1 };
28856
+ this._cropDragState = null;
28857
+ this._cropToolActive = false;
28858
+ if (swiperElement) {
28859
+ this._applyZoom(swiperElement);
28860
+ this._updateEditImageSize(swiperElement);
28861
+ }
28862
+ }
28863
+ _setupWheelZoom(swiperElement) {
28864
+ this._cleanupWheelZoom();
28865
+ if (!this.mouseZoomEnabled)
28866
+ return;
28867
+ const el = swiperElement?.nativeElement ?? swiperElement;
28868
+ if (!el)
28869
+ return;
28870
+ this._wheelListener = (event) => {
28871
+ event.preventDefault();
28872
+ const step = this.wheelZoomStep;
28873
+ const oldScale = this._zoomState.scale;
28874
+ const newScale = Math.max(1, Math.min(oldScale + (event.deltaY < 0 ? step : -step), this.maxZoomRatio));
28875
+ if (newScale === oldScale)
28876
+ return;
28877
+ if (newScale === 1) {
28878
+ this._zoomState = { scale: 1, tx: 0, ty: 0 };
28879
+ }
28880
+ else {
28881
+ // Zoom towards cursor position
28882
+ const swiper = this.getSwiper(el);
28883
+ const activeSlide = swiper?.slides?.[swiper.activeIndex];
28884
+ if (activeSlide) {
28885
+ const rect = activeSlide.getBoundingClientRect();
28886
+ const cx = event.clientX - rect.left - rect.width / 2;
28887
+ const cy = event.clientY - rect.top - rect.height / 2;
28888
+ this._zoomState.tx = cx - ((cx - this._zoomState.tx) * newScale) / oldScale;
28889
+ this._zoomState.ty = cy - ((cy - this._zoomState.ty) * newScale) / oldScale;
28890
+ }
28891
+ this._zoomState.scale = newScale;
28892
+ this._clampTranslations(el);
28893
+ }
28894
+ this._applyZoom(el);
28895
+ };
28896
+ el.addEventListener('wheel', this._wheelListener, { passive: false });
28897
+ this._wheelEl = el;
28898
+ }
28899
+ _cleanupWheelZoom() {
28900
+ if (this._wheelEl && this._wheelListener) {
28901
+ this._wheelEl.removeEventListener('wheel', this._wheelListener);
28902
+ }
28903
+ this._wheelEl = null;
28904
+ this._wheelListener = null;
28905
+ }
28906
+ _applyZoom(swiperElement) {
28907
+ const swiper = this.getSwiper(swiperElement);
28908
+ const activeSlide = swiper?.slides?.[swiper.activeIndex];
28909
+ if (!activeSlide)
28910
+ return;
28911
+ const wrapEl = activeSlide.querySelector('.swiper-zoom-container');
28912
+ const imgEl = activeSlide.querySelector('.swiper-zoom-target, .swiper-zoom-container > img');
28913
+ if (!wrapEl || !imgEl)
28914
+ return;
28915
+ const { scale, tx, ty } = this._zoomState;
28916
+ wrapEl.style.transitionDuration = '0ms';
28917
+ wrapEl.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
28918
+ imgEl.style.transitionDuration = '0ms';
28919
+ imgEl.style.transform = `translate3d(0, 0, 0) scale(${scale}) rotate(${this._rotation}deg)`;
28920
+ wrapEl.style.cursor = scale > 1 ? 'grab' : '';
28921
+ }
28922
+ _clampTranslations(swiperElement) {
28923
+ const swiper = this.getSwiper(swiperElement);
28924
+ const activeSlide = swiper?.slides?.[swiper.activeIndex];
28925
+ if (!activeSlide)
28926
+ return;
28927
+ const imgEl = activeSlide.querySelector('.swiper-zoom-target, .swiper-zoom-container > img');
28928
+ if (!imgEl)
28929
+ return;
28930
+ const imgW = imgEl.offsetWidth || imgEl.clientWidth;
28931
+ const imgH = imgEl.offsetHeight || imgEl.clientHeight;
28932
+ if (!imgW || !imgH)
28933
+ return;
28934
+ const { scale } = this._zoomState;
28935
+ const maxTx = Math.max(0, (imgW * scale - activeSlide.offsetWidth) / 2);
28936
+ const maxTy = Math.max(0, (imgH * scale - activeSlide.offsetHeight) / 2);
28937
+ this._zoomState.tx = Math.max(-maxTx, Math.min(maxTx, this._zoomState.tx));
28938
+ this._zoomState.ty = Math.max(-maxTy, Math.min(maxTy, this._zoomState.ty));
28939
+ }
28940
+ _getActiveZoomWrapEl(swiperElement) {
28941
+ const swiper = this.getSwiper(swiperElement);
28942
+ const activeSlide = swiper?.slides?.[swiper.activeIndex];
28943
+ return activeSlide?.querySelector('.swiper-zoom-container') ?? null;
28944
+ }
28945
+ markForCheck() {
28946
+ this.cd.markForCheck();
28947
+ }
28948
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AppImageGallerySlideshowComponent, deps: [{ token: i2$1.AlertController }, { token: i1$1.TranslateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
28949
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: AppImageGallerySlideshowComponent, selector: "app-image-gallery-slideshow", inputs: { rows: "rows", selectedIndex: "selectedIndex", mobile: ["mobile", "mobile", booleanAttribute], readOnly: ["readOnly", "readOnly", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], debug: ["debug", "debug", booleanAttribute], mouseZoomEnabled: ["mouseZoomEnabled", "mouseZoomEnabled", booleanAttribute], wheelZoomStep: "wheelZoomStep", maxZoomRatio: "maxZoomRatio", rotationEnabled: ["rotationEnabled", "rotationEnabled", booleanAttribute], cropEnabled: ["cropEnabled", "cropEnabled", booleanAttribute], imageExportFormat: "imageExportFormat", imageExportQuality: "imageExportQuality", imageSizeQueryParam: "imageSizeQueryParam", modalImageSize: "modalImageSize" }, outputs: { deleteRequested: "deleteRequested", closeRequested: "closeRequested", afterEditImage: "afterEditImage", afterEditRow: "afterEditRow" }, host: { listeners: { "window:resize": "onWindowResize()" } }, viewQueries: [{ propertyName: "swiperRef", first: true, predicate: ["slideshowSwiper"], descendants: true }], ngImport: i0, template: "<ion-content\n class=\"ion-no-padding ion-no-margin\"\n (mousedown)=\"onPointerDown($event, slideshowSwiper)\"\n (mousemove)=\"onPointerMove($event, slideshowSwiper)\"\n (mouseup)=\"onPointerUp($event, slideshowSwiper)\"\n (mouseleave)=\"onPointerUp($event, slideshowSwiper)\"\n (touchmove)=\"onCropPointerMove($event, slideshowSwiper)\"\n (touchend)=\"onCropPointerUp($event, slideshowSwiper)\"\n (touchcancel)=\"onCropPointerUp($event, slideshowSwiper)\"\n>\n @let count = rows?.length || 0;\n <!-- Counter -->\n <div class=\"top-left\">\n <ion-label color=\"light\">{{ activeSlideIndex + 1 }} / {{ count }}</ion-label>\n </div>\n\n <!-- zoom / rotation / crop buttons: all shown together, no \"enter edit mode\" step needed -->\n <div class=\"top-right\">\n @if (!mobile) {\n <ion-fab-button size=\"small\" (click)=\"zoom(slideshowSwiper, true)\" color=\"light\">\n <ion-icon name=\"search\"></ion-icon>\n <ion-icon class=\"icon-secondary\" name=\"add\"></ion-icon>\n </ion-fab-button>\n <ion-fab-button size=\"small\" (click)=\"zoom(slideshowSwiper, false)\" color=\"light\">\n <ion-icon name=\"search\"></ion-icon>\n <ion-icon class=\"icon-secondary\" name=\"remove\"></ion-icon>\n </ion-fab-button>\n }\n @if (!readOnly && rotationEnabled) {\n <ion-fab-button\n size=\"small\"\n (click)=\"rotate(slideshowSwiper, 'left')\"\n color=\"light\"\n [title]=\"!showTooltip ? ('IMAGE.GALLERY.BTN_ROTATE_LEFT' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('IMAGE.GALLERY.BTN_ROTATE_LEFT' | translate) : ''\"\n >\n <mat-icon>rotate_left</mat-icon>\n </ion-fab-button>\n <ion-fab-button\n size=\"small\"\n (click)=\"rotate(slideshowSwiper, 'right')\"\n color=\"light\"\n [title]=\"!showTooltip ? ('IMAGE.GALLERY.BTN_ROTATE_RIGHT' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('IMAGE.GALLERY.BTN_ROTATE_RIGHT' | translate) : ''\"\n >\n <mat-icon>rotate_right</mat-icon>\n </ion-fab-button>\n }\n @if (!readOnly && cropEnabled) {\n <ion-fab-button\n size=\"small\"\n (click)=\"toggleCropTool(slideshowSwiper)\"\n [color]=\"_cropToolActive ? 'tertiary' : 'light'\"\n [title]=\"\n !showTooltip\n ? ((_cropToolActive ? 'IMAGE.GALLERY.BTN_CROP_DEACTIVATE' : 'IMAGE.GALLERY.BTN_CROP_ACTIVATE') | translate)\n : ''\n \"\n [matTooltip]=\"\n showTooltip\n ? ((_cropToolActive ? 'IMAGE.GALLERY.BTN_CROP_DEACTIVATE' : 'IMAGE.GALLERY.BTN_CROP_ACTIVATE') | translate)\n : ''\n \"\n >\n <mat-icon>crop</mat-icon>\n </ion-fab-button>\n @if (_cropToolActive) {\n <ion-fab-button\n size=\"small\"\n (click)=\"resetCrop(slideshowSwiper)\"\n color=\"light\"\n [title]=\"!showTooltip ? ('IMAGE.GALLERY.BTN_CROP_RESET' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('IMAGE.GALLERY.BTN_CROP_RESET' | translate) : ''\"\n >\n <mat-icon>crop_free</mat-icon>\n </ion-fab-button>\n }\n }\n </div>\n\n <!-- navigation side buttons next/back -->\n <ng-container *ngIf=\"!mobile && (rows | isArrayLength: { greaterThan: 1 })\">\n <div class=\"side-buttons prev\" (click)=\"slidePrev(slideshowSwiper)\">\n <ion-icon name=\"chevron-back\"></ion-icon>\n </div>\n <div class=\"side-buttons next\" (click)=\"slideNext(slideshowSwiper)\">\n <ion-icon name=\"chevron-forward\"></ion-icon>\n </div>\n </ng-container>\n\n <!-- image slides -->\n <swiper-container\n #slideshowSwiper\n [modules]=\"swiperModules\"\n [zoom]=\"!mouseZoomEnabled\"\n (swiperslidechange)=\"onSlideChange(slideshowSwiper)\"\n (swiperinit)=\"onSwiperInit(slideshowSwiper)\"\n >\n @for (row of rows; track row.id; let i = $index) {\n <swiper-slide>\n @let image = row | propertyGet: 'currentData';\n <div class=\"swiper-zoom-container\">\n @if (image.url) {\n <ion-img class=\"swiper-zoom-target\" [src]=\"image.url | appendQueryParams: getImageSizeQueryParams()\" />\n } @else {\n <img [src]=\"image.dataUrl\" [alt]=\"image.title\" />\n }\n\n <!-- crop overlay: only rendered for the active slide -->\n @if (!readOnly && cropEnabled && _cropToolActive && i === activeSlideIndex) {\n <div\n class=\"crop-overlay\"\n (mousedown)=\"onCropPointerDown($event, slideshowSwiper, 'move')\"\n (touchstart)=\"onCropPointerDown($event, slideshowSwiper, 'move')\"\n >\n <!-- darkened mask around the selection: clipped to the image bounds -->\n <div class=\"crop-mask\">\n <div\n class=\"crop-mask-rect\"\n [style.left.%]=\"_cropRect.x * 100\"\n [style.top.%]=\"_cropRect.y * 100\"\n [style.width.%]=\"_cropRect.width * 100\"\n [style.height.%]=\"_cropRect.height * 100\"\n ></div>\n </div>\n\n <!-- interactive selection border, grid & drag handles: never clipped, so corner/edge handles\n remain fully grabbable even when the selection touches the image bounds -->\n <div\n class=\"crop-rect\"\n [style.left.%]=\"_cropRect.x * 100\"\n [style.top.%]=\"_cropRect.y * 100\"\n [style.width.%]=\"_cropRect.width * 100\"\n [style.height.%]=\"_cropRect.height * 100\"\n >\n <div class=\"crop-grid\">\n <span class=\"line v\" style=\"left: 33.333%\"></span>\n <span class=\"line v\" style=\"left: 66.666%\"></span>\n <span class=\"line h\" style=\"top: 33.333%\"></span>\n <span class=\"line h\" style=\"top: 66.666%\"></span>\n </div>\n @for (handle of _cropHandles; track handle) {\n <div\n class=\"crop-handle {{ handle }}\"\n (mousedown)=\"onCropPointerDown($event, slideshowSwiper, handle)\"\n (touchstart)=\"onCropPointerDown($event, slideshowSwiper, handle)\"\n ></div>\n }\n </div>\n </div>\n }\n </div>\n </swiper-slide>\n }\n </swiper-container>\n</ion-content>\n\n<ion-footer>\n <ion-toolbar color=\"light\">\n @if (!hasPendingEdit) {\n <ion-row>\n <ion-col size=\"4\" class=\"ion-text-start\">\n <ion-button\n *ngIf=\"!readOnly\"\n (click)=\"requestDelete(slideshowSwiper)\"\n [disabled]=\"disabled\"\n fill=\"clear\"\n color=\"danger\"\n >\n <mat-icon slot=\"start\">delete</mat-icon>\n {{ 'COMMON.BTN_DELETE' | translate }}\n </ion-button>\n </ion-col>\n <ion-col size=\"4\"></ion-col>\n <ion-col size=\"4\" class=\"ion-text-end\">\n <ion-button (click)=\"closeRequested.emit()\" color=\"tertiary\" fill=\"clear\" *ngIf=\"mobile; else desktop\">\n <ion-icon slot=\"start\" name=\"close\"></ion-icon>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n <ng-template #desktop>\n <ion-button (click)=\"closeRequested.emit()\" color=\"tertiary\">\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ng-template>\n </ion-col>\n </ion-row>\n } @else {\n <!-- A crop or rotation change is pending: ask for an explicit validation before exporting the image -->\n <ion-row>\n <ion-col size=\"6\" class=\"ion-text-start\">\n <ion-button (click)=\"discardEdit(slideshowSwiper)\" color=\"medium\" fill=\"clear\">\n <ion-icon slot=\"start\" name=\"close\"></ion-icon>\n {{ 'COMMON.BTN_CANCEL' | translate }}\n </ion-button>\n </ion-col>\n <ion-col size=\"6\" class=\"ion-text-end\">\n <ion-button (click)=\"validateEdit(slideshowSwiper)\" color=\"tertiary\">\n <ion-icon slot=\"start\" name=\"checkmark\"></ion-icon>\n {{ 'COMMON.BTN_VALIDATE' | translate }}\n </ion-button>\n </ion-col>\n </ion-row>\n }\n </ion-toolbar>\n</ion-footer>\n", styles: [":host{display:flex;flex-direction:column;height:100%;-webkit-user-select:none;user-select:none}:host ion-content{flex:1 1 auto}:host .top-left{z-index:14;position:absolute;display:inline-grid;left:var(--ion-padding-start, 8px);top:var(--ion-padding-start, 8px)}:host .top-right{z-index:14;position:absolute;display:inline-grid;right:var(--ion-padding-start, 8px);top:var(--ion-padding-start, 8px)}:host .side-buttons{--side-buttons-width: 120px;--side-buttons-margin: calc(var(--side-buttons-width) / 3);z-index:13;position:absolute;top:0;bottom:0;width:var(--side-buttons-width)}:host .side-buttons ion-icon{top:50%;color:#fff;font-size:2.5rem;position:absolute;opacity:0;transition-duration:.15s;transition-timing-function:ease-in}:host .side-buttons:hover{cursor:pointer}:host .side-buttons:hover ion-icon{opacity:1}:host .side-buttons.prev{left:0}:host .side-buttons.prev ion-icon{left:var(--side-buttons-margin)}:host .side-buttons.next{right:0}:host .side-buttons.next ion-icon{right:var(--side-buttons-margin)}:host swiper-container{height:100%;background:#000}:host .swiper-zoom-container{position:relative}:host ion-fab-button .icon-secondary{top:11px;left:6px;height:14px}:host .crop-overlay{position:absolute;z-index:15;cursor:move;touch-action:none;animation:crop-overlay-fade-in .15s ease-in}:host .crop-overlay .crop-mask{position:absolute;inset:0;overflow:hidden;pointer-events:none}:host .crop-overlay .crop-mask .crop-mask-rect{position:absolute;box-shadow:0 0 0 2000px #0009}:host .crop-overlay .crop-rect{position:absolute;box-sizing:border-box;border:1px solid rgba(255,255,255,.95);cursor:move;touch-action:none}:host .crop-overlay .crop-rect .crop-grid{position:absolute;inset:0;pointer-events:none}:host .crop-overlay .crop-rect .crop-grid .line{position:absolute;background:#ffffff73}:host .crop-overlay .crop-rect .crop-grid .line.v{top:0;bottom:0;width:1px}:host .crop-overlay .crop-rect .crop-grid .line.h{left:0;right:0;height:1px}:host .crop-overlay .crop-rect .crop-handle{position:absolute;width:16px;height:16px;margin:-8px;background:#fff;border:1px solid rgba(0,0,0,.45);border-radius:50%;box-shadow:0 1px 4px #00000080;transition:transform .12s ease;touch-action:none}:host .crop-overlay .crop-rect .crop-handle:hover{transform:scale(1.3)}:host .crop-overlay .crop-rect .crop-handle.nw{left:0;top:0;cursor:nwse-resize}:host .crop-overlay .crop-rect .crop-handle.n{left:50%;top:0;cursor:ns-resize}:host .crop-overlay .crop-rect .crop-handle.ne{left:100%;top:0;cursor:nesw-resize}:host .crop-overlay .crop-rect .crop-handle.e{left:100%;top:50%;cursor:ew-resize}:host .crop-overlay .crop-rect .crop-handle.se{left:100%;top:100%;cursor:nwse-resize}:host .crop-overlay .crop-rect .crop-handle.s{left:50%;top:100%;cursor:ns-resize}:host .crop-overlay .crop-rect .crop-handle.sw{left:0;top:100%;cursor:nesw-resize}:host .crop-overlay .crop-rect .crop-handle.w{left:0;top:50%;cursor:ew-resize}@keyframes crop-overlay-fade-in{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonFabButton, selector: "ion-fab-button", inputs: ["activated", "closeIcon", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "show", "size", "target", "translucent", "type"] }, { kind: "component", type: i2$1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i1$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: PropertyGetPipe, name: "propertyGet" }, { kind: "pipe", type: ArrayLengthPipe, name: "isArrayLength" }, { kind: "pipe", type: AppendQueryParamsPipePipe, name: "appendQueryParams" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
28950
+ }
28951
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AppImageGallerySlideshowComponent, decorators: [{
28952
+ type: Component,
28953
+ args: [{ selector: 'app-image-gallery-slideshow', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-content\n class=\"ion-no-padding ion-no-margin\"\n (mousedown)=\"onPointerDown($event, slideshowSwiper)\"\n (mousemove)=\"onPointerMove($event, slideshowSwiper)\"\n (mouseup)=\"onPointerUp($event, slideshowSwiper)\"\n (mouseleave)=\"onPointerUp($event, slideshowSwiper)\"\n (touchmove)=\"onCropPointerMove($event, slideshowSwiper)\"\n (touchend)=\"onCropPointerUp($event, slideshowSwiper)\"\n (touchcancel)=\"onCropPointerUp($event, slideshowSwiper)\"\n>\n @let count = rows?.length || 0;\n <!-- Counter -->\n <div class=\"top-left\">\n <ion-label color=\"light\">{{ activeSlideIndex + 1 }} / {{ count }}</ion-label>\n </div>\n\n <!-- zoom / rotation / crop buttons: all shown together, no \"enter edit mode\" step needed -->\n <div class=\"top-right\">\n @if (!mobile) {\n <ion-fab-button size=\"small\" (click)=\"zoom(slideshowSwiper, true)\" color=\"light\">\n <ion-icon name=\"search\"></ion-icon>\n <ion-icon class=\"icon-secondary\" name=\"add\"></ion-icon>\n </ion-fab-button>\n <ion-fab-button size=\"small\" (click)=\"zoom(slideshowSwiper, false)\" color=\"light\">\n <ion-icon name=\"search\"></ion-icon>\n <ion-icon class=\"icon-secondary\" name=\"remove\"></ion-icon>\n </ion-fab-button>\n }\n @if (!readOnly && rotationEnabled) {\n <ion-fab-button\n size=\"small\"\n (click)=\"rotate(slideshowSwiper, 'left')\"\n color=\"light\"\n [title]=\"!showTooltip ? ('IMAGE.GALLERY.BTN_ROTATE_LEFT' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('IMAGE.GALLERY.BTN_ROTATE_LEFT' | translate) : ''\"\n >\n <mat-icon>rotate_left</mat-icon>\n </ion-fab-button>\n <ion-fab-button\n size=\"small\"\n (click)=\"rotate(slideshowSwiper, 'right')\"\n color=\"light\"\n [title]=\"!showTooltip ? ('IMAGE.GALLERY.BTN_ROTATE_RIGHT' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('IMAGE.GALLERY.BTN_ROTATE_RIGHT' | translate) : ''\"\n >\n <mat-icon>rotate_right</mat-icon>\n </ion-fab-button>\n }\n @if (!readOnly && cropEnabled) {\n <ion-fab-button\n size=\"small\"\n (click)=\"toggleCropTool(slideshowSwiper)\"\n [color]=\"_cropToolActive ? 'tertiary' : 'light'\"\n [title]=\"\n !showTooltip\n ? ((_cropToolActive ? 'IMAGE.GALLERY.BTN_CROP_DEACTIVATE' : 'IMAGE.GALLERY.BTN_CROP_ACTIVATE') | translate)\n : ''\n \"\n [matTooltip]=\"\n showTooltip\n ? ((_cropToolActive ? 'IMAGE.GALLERY.BTN_CROP_DEACTIVATE' : 'IMAGE.GALLERY.BTN_CROP_ACTIVATE') | translate)\n : ''\n \"\n >\n <mat-icon>crop</mat-icon>\n </ion-fab-button>\n @if (_cropToolActive) {\n <ion-fab-button\n size=\"small\"\n (click)=\"resetCrop(slideshowSwiper)\"\n color=\"light\"\n [title]=\"!showTooltip ? ('IMAGE.GALLERY.BTN_CROP_RESET' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('IMAGE.GALLERY.BTN_CROP_RESET' | translate) : ''\"\n >\n <mat-icon>crop_free</mat-icon>\n </ion-fab-button>\n }\n }\n </div>\n\n <!-- navigation side buttons next/back -->\n <ng-container *ngIf=\"!mobile && (rows | isArrayLength: { greaterThan: 1 })\">\n <div class=\"side-buttons prev\" (click)=\"slidePrev(slideshowSwiper)\">\n <ion-icon name=\"chevron-back\"></ion-icon>\n </div>\n <div class=\"side-buttons next\" (click)=\"slideNext(slideshowSwiper)\">\n <ion-icon name=\"chevron-forward\"></ion-icon>\n </div>\n </ng-container>\n\n <!-- image slides -->\n <swiper-container\n #slideshowSwiper\n [modules]=\"swiperModules\"\n [zoom]=\"!mouseZoomEnabled\"\n (swiperslidechange)=\"onSlideChange(slideshowSwiper)\"\n (swiperinit)=\"onSwiperInit(slideshowSwiper)\"\n >\n @for (row of rows; track row.id; let i = $index) {\n <swiper-slide>\n @let image = row | propertyGet: 'currentData';\n <div class=\"swiper-zoom-container\">\n @if (image.url) {\n <ion-img class=\"swiper-zoom-target\" [src]=\"image.url | appendQueryParams: getImageSizeQueryParams()\" />\n } @else {\n <img [src]=\"image.dataUrl\" [alt]=\"image.title\" />\n }\n\n <!-- crop overlay: only rendered for the active slide -->\n @if (!readOnly && cropEnabled && _cropToolActive && i === activeSlideIndex) {\n <div\n class=\"crop-overlay\"\n (mousedown)=\"onCropPointerDown($event, slideshowSwiper, 'move')\"\n (touchstart)=\"onCropPointerDown($event, slideshowSwiper, 'move')\"\n >\n <!-- darkened mask around the selection: clipped to the image bounds -->\n <div class=\"crop-mask\">\n <div\n class=\"crop-mask-rect\"\n [style.left.%]=\"_cropRect.x * 100\"\n [style.top.%]=\"_cropRect.y * 100\"\n [style.width.%]=\"_cropRect.width * 100\"\n [style.height.%]=\"_cropRect.height * 100\"\n ></div>\n </div>\n\n <!-- interactive selection border, grid & drag handles: never clipped, so corner/edge handles\n remain fully grabbable even when the selection touches the image bounds -->\n <div\n class=\"crop-rect\"\n [style.left.%]=\"_cropRect.x * 100\"\n [style.top.%]=\"_cropRect.y * 100\"\n [style.width.%]=\"_cropRect.width * 100\"\n [style.height.%]=\"_cropRect.height * 100\"\n >\n <div class=\"crop-grid\">\n <span class=\"line v\" style=\"left: 33.333%\"></span>\n <span class=\"line v\" style=\"left: 66.666%\"></span>\n <span class=\"line h\" style=\"top: 33.333%\"></span>\n <span class=\"line h\" style=\"top: 66.666%\"></span>\n </div>\n @for (handle of _cropHandles; track handle) {\n <div\n class=\"crop-handle {{ handle }}\"\n (mousedown)=\"onCropPointerDown($event, slideshowSwiper, handle)\"\n (touchstart)=\"onCropPointerDown($event, slideshowSwiper, handle)\"\n ></div>\n }\n </div>\n </div>\n }\n </div>\n </swiper-slide>\n }\n </swiper-container>\n</ion-content>\n\n<ion-footer>\n <ion-toolbar color=\"light\">\n @if (!hasPendingEdit) {\n <ion-row>\n <ion-col size=\"4\" class=\"ion-text-start\">\n <ion-button\n *ngIf=\"!readOnly\"\n (click)=\"requestDelete(slideshowSwiper)\"\n [disabled]=\"disabled\"\n fill=\"clear\"\n color=\"danger\"\n >\n <mat-icon slot=\"start\">delete</mat-icon>\n {{ 'COMMON.BTN_DELETE' | translate }}\n </ion-button>\n </ion-col>\n <ion-col size=\"4\"></ion-col>\n <ion-col size=\"4\" class=\"ion-text-end\">\n <ion-button (click)=\"closeRequested.emit()\" color=\"tertiary\" fill=\"clear\" *ngIf=\"mobile; else desktop\">\n <ion-icon slot=\"start\" name=\"close\"></ion-icon>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n <ng-template #desktop>\n <ion-button (click)=\"closeRequested.emit()\" color=\"tertiary\">\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ng-template>\n </ion-col>\n </ion-row>\n } @else {\n <!-- A crop or rotation change is pending: ask for an explicit validation before exporting the image -->\n <ion-row>\n <ion-col size=\"6\" class=\"ion-text-start\">\n <ion-button (click)=\"discardEdit(slideshowSwiper)\" color=\"medium\" fill=\"clear\">\n <ion-icon slot=\"start\" name=\"close\"></ion-icon>\n {{ 'COMMON.BTN_CANCEL' | translate }}\n </ion-button>\n </ion-col>\n <ion-col size=\"6\" class=\"ion-text-end\">\n <ion-button (click)=\"validateEdit(slideshowSwiper)\" color=\"tertiary\">\n <ion-icon slot=\"start\" name=\"checkmark\"></ion-icon>\n {{ 'COMMON.BTN_VALIDATE' | translate }}\n </ion-button>\n </ion-col>\n </ion-row>\n }\n </ion-toolbar>\n</ion-footer>\n", styles: [":host{display:flex;flex-direction:column;height:100%;-webkit-user-select:none;user-select:none}:host ion-content{flex:1 1 auto}:host .top-left{z-index:14;position:absolute;display:inline-grid;left:var(--ion-padding-start, 8px);top:var(--ion-padding-start, 8px)}:host .top-right{z-index:14;position:absolute;display:inline-grid;right:var(--ion-padding-start, 8px);top:var(--ion-padding-start, 8px)}:host .side-buttons{--side-buttons-width: 120px;--side-buttons-margin: calc(var(--side-buttons-width) / 3);z-index:13;position:absolute;top:0;bottom:0;width:var(--side-buttons-width)}:host .side-buttons ion-icon{top:50%;color:#fff;font-size:2.5rem;position:absolute;opacity:0;transition-duration:.15s;transition-timing-function:ease-in}:host .side-buttons:hover{cursor:pointer}:host .side-buttons:hover ion-icon{opacity:1}:host .side-buttons.prev{left:0}:host .side-buttons.prev ion-icon{left:var(--side-buttons-margin)}:host .side-buttons.next{right:0}:host .side-buttons.next ion-icon{right:var(--side-buttons-margin)}:host swiper-container{height:100%;background:#000}:host .swiper-zoom-container{position:relative}:host ion-fab-button .icon-secondary{top:11px;left:6px;height:14px}:host .crop-overlay{position:absolute;z-index:15;cursor:move;touch-action:none;animation:crop-overlay-fade-in .15s ease-in}:host .crop-overlay .crop-mask{position:absolute;inset:0;overflow:hidden;pointer-events:none}:host .crop-overlay .crop-mask .crop-mask-rect{position:absolute;box-shadow:0 0 0 2000px #0009}:host .crop-overlay .crop-rect{position:absolute;box-sizing:border-box;border:1px solid rgba(255,255,255,.95);cursor:move;touch-action:none}:host .crop-overlay .crop-rect .crop-grid{position:absolute;inset:0;pointer-events:none}:host .crop-overlay .crop-rect .crop-grid .line{position:absolute;background:#ffffff73}:host .crop-overlay .crop-rect .crop-grid .line.v{top:0;bottom:0;width:1px}:host .crop-overlay .crop-rect .crop-grid .line.h{left:0;right:0;height:1px}:host .crop-overlay .crop-rect .crop-handle{position:absolute;width:16px;height:16px;margin:-8px;background:#fff;border:1px solid rgba(0,0,0,.45);border-radius:50%;box-shadow:0 1px 4px #00000080;transition:transform .12s ease;touch-action:none}:host .crop-overlay .crop-rect .crop-handle:hover{transform:scale(1.3)}:host .crop-overlay .crop-rect .crop-handle.nw{left:0;top:0;cursor:nwse-resize}:host .crop-overlay .crop-rect .crop-handle.n{left:50%;top:0;cursor:ns-resize}:host .crop-overlay .crop-rect .crop-handle.ne{left:100%;top:0;cursor:nesw-resize}:host .crop-overlay .crop-rect .crop-handle.e{left:100%;top:50%;cursor:ew-resize}:host .crop-overlay .crop-rect .crop-handle.se{left:100%;top:100%;cursor:nwse-resize}:host .crop-overlay .crop-rect .crop-handle.s{left:50%;top:100%;cursor:ns-resize}:host .crop-overlay .crop-rect .crop-handle.sw{left:0;top:100%;cursor:nesw-resize}:host .crop-overlay .crop-rect .crop-handle.w{left:0;top:50%;cursor:ew-resize}@keyframes crop-overlay-fade-in{0%{opacity:0}to{opacity:1}}\n"] }]
28954
+ }], ctorParameters: () => [{ type: i2$1.AlertController }, { type: i1$1.TranslateService }, { type: i0.ChangeDetectorRef }], propDecorators: { rows: [{
28955
+ type: Input
28956
+ }], selectedIndex: [{
28957
+ type: Input
28958
+ }], mobile: [{
28959
+ type: Input,
28960
+ args: [{ transform: booleanAttribute }]
28961
+ }], readOnly: [{
28962
+ type: Input,
28963
+ args: [{ transform: booleanAttribute }]
28964
+ }], disabled: [{
28965
+ type: Input,
28966
+ args: [{ transform: booleanAttribute }]
28967
+ }], debug: [{
28968
+ type: Input,
28969
+ args: [{ transform: booleanAttribute }]
28970
+ }], mouseZoomEnabled: [{
28971
+ type: Input,
28972
+ args: [{ transform: booleanAttribute }]
28973
+ }], wheelZoomStep: [{
28974
+ type: Input
28975
+ }], maxZoomRatio: [{
28976
+ type: Input
28977
+ }], rotationEnabled: [{
28978
+ type: Input,
28979
+ args: [{ transform: booleanAttribute }]
28980
+ }], cropEnabled: [{
28981
+ type: Input,
28982
+ args: [{ transform: booleanAttribute }]
28983
+ }], imageExportFormat: [{
28984
+ type: Input
28985
+ }], imageExportQuality: [{
28986
+ type: Input
28987
+ }], imageSizeQueryParam: [{
28988
+ type: Input
28989
+ }], modalImageSize: [{
28990
+ type: Input
28991
+ }], deleteRequested: [{
28992
+ type: Output
28993
+ }], closeRequested: [{
28994
+ type: Output
28995
+ }], afterEditImage: [{
28996
+ type: Output
28997
+ }], afterEditRow: [{
28998
+ type: Output
28999
+ }], swiperRef: [{
29000
+ type: ViewChild,
29001
+ args: ['slideshowSwiper']
29002
+ }], onWindowResize: [{
29003
+ type: HostListener,
29004
+ args: ['window:resize']
29005
+ }] } });
29006
+
28337
29007
  class ImageService extends StartableService {
28338
29008
  platform;
28339
29009
  settings;
@@ -28459,10 +29129,13 @@ class AppImageGalleryComponent {
28459
29129
  _titleForm;
28460
29130
  _titleAutofocus = false;
28461
29131
  _slidesSubscription;
28462
- _swiper;
28463
- swiperModules = [IonicSlides];
28464
29132
  zoomActive = false;
28465
29133
  zoomScale = 1;
29134
+ _mouseZoomEnabled = false;
29135
+ _rotationEnabled = false;
29136
+ _cropEnabled = false;
29137
+ /** Whether the inline slideshow overlay is currently shown (only relevant when zoomPresentation === 'inline'). */
29138
+ _inlineSlideshowActive = false;
28466
29139
  viewChange;
28467
29140
  cardColor;
28468
29141
  debug = null;
@@ -28487,6 +29160,24 @@ class AppImageGalleryComponent {
28487
29160
  modal: 'large',
28488
29161
  };
28489
29162
  imageSizeQueryParam = 'size';
29163
+ /** Enable mouse-wheel zoom and mouse-drag pan in the slideshow (defaults to true on desktop). */
29164
+ enableMouseZoom = null;
29165
+ /** Zoom step applied on each mouse-wheel tick (default 0.3). */
29166
+ wheelZoomStep = 0.3;
29167
+ /** Maximum zoom ratio reachable via mouse wheel (default 3). */
29168
+ maxZoomRatio = 3;
29169
+ /** Enable 90° rotation buttons in the slideshow (defaults to true). */
29170
+ enableRotation = null;
29171
+ /** Enable the crop tool in the slideshow (defaults to true). */
29172
+ enableCrop = null;
29173
+ /** Image format used when emitting the edited (cropped/rotated) image. */
29174
+ imageExportFormat = 'png';
29175
+ /** Export quality, applied only when imageExportFormat is 'jpeg' (0 to 1). */
29176
+ imageExportQuality = 0.92;
29177
+ /** Where to show the slideshow: in a full-screen modal (default), or inline, as an overlay over the gallery itself (desktop only). */
29178
+ zoomPresentation = 'inline';
29179
+ /** Height of the inline slideshow overlay (CSS value, e.g. '60vh'). Only used when zoomPresentation is 'inline'. */
29180
+ inlineZoomHeight = '100%';
28490
29181
  set dataSource(dataSource) {
28491
29182
  if (this._dataSource !== dataSource) {
28492
29183
  this._switchDataSource(dataSource);
@@ -28504,15 +29195,12 @@ class AppImageGalleryComponent {
28504
29195
  get rowCount() {
28505
29196
  return this.rows?.length || 0;
28506
29197
  }
28507
- get activeIndex() {
28508
- return this.getSwiper()?.activeIndex || 0;
28509
- }
28510
29198
  zoomModal;
28511
29199
  titlePopover;
28512
- swiperRef;
28513
29200
  onBeforeDeleteRows = createPromiseEventEmitter();
28514
29201
  onAfterAddRows = new EventEmitter();
28515
29202
  onAfterEditRow = new EventEmitter();
29203
+ onAfterEditImage = new EventEmitter();
28516
29204
  click = new EventEmitter();
28517
29205
  showTooltip = inject(APP_SHOW_TOOLTIP, { optional: true });
28518
29206
  constructor(imageService, platform, alterCtrl, translate, cd, environment) {
@@ -28534,6 +29222,13 @@ class AppImageGalleryComponent {
28534
29222
  this.showAddCardButton = toBoolean(this.showAddCardButton, this.mobile);
28535
29223
  this.showFabButton = toBoolean(this.showFabButton, this.mobile && !this.showAddCardButton);
28536
29224
  this.showAddToolbarButton = this.showAddToolbarButton ?? (this.showToolbar && !this.readOnly);
29225
+ this._mouseZoomEnabled = toBoolean(this.enableMouseZoom, !this.mobile);
29226
+ this._rotationEnabled = toBoolean(this.enableRotation, true);
29227
+ this._cropEnabled = toBoolean(this.enableCrop, true);
29228
+ // The inline presentation is only meaningful on desktop: there is no "rest of the page" worth
29229
+ // preserving on a small mobile screen, and the fullscreen modal already works well there.
29230
+ if (this.mobile)
29231
+ this.zoomPresentation = 'modal';
28537
29232
  // Get modal presenting element (iOS only)
28538
29233
  this._presentingElement = this.mobile ? document.querySelector('.ion-page') : null;
28539
29234
  }
@@ -28664,82 +29359,38 @@ class AppImageGalleryComponent {
28664
29359
  // Delegate click action
28665
29360
  if (this.click.observed) {
28666
29361
  this.click.emit(row.currentData);
29362
+ return;
29363
+ }
29364
+ // By default, open the slideshow: either inline (overlay in the gallery itself), or in a full-screen modal
29365
+ this._selectedRowIndex = index;
29366
+ if (this.zoomPresentation === 'inline') {
29367
+ this._inlineSlideshowActive = true;
29368
+ this.markForCheck();
28667
29369
  }
28668
- // By default, open a zoom modal
28669
29370
  else {
28670
- this._selectedRowIndex = index;
28671
29371
  await this.zoomModal.present();
28672
29372
  await this.zoomModal.onDidDismiss();
28673
29373
  }
28674
29374
  }
28675
- activeSlideIndex = 0;
28676
- getSwiper(swiperElement) {
28677
- this._swiper = (swiperElement || this.swiperRef?.nativeElement)?.swiper || this._swiper;
28678
- return this._swiper;
28679
- }
28680
- onSlideChange(swiperElement) {
28681
- const swiper = this.getSwiper(swiperElement);
28682
- this.activeSlideIndex = swiper.activeIndex;
29375
+ closeInlineSlideshow() {
29376
+ this._inlineSlideshowActive = false;
28683
29377
  this.markForCheck();
28684
29378
  }
28685
- initModalSlides(swiperElement) {
28686
- if (this._selectedRowIndex > 0) {
28687
- if (this.debug)
28688
- console.debug(`[image-gallery] Initializing modal, selectedRowIndex=${this._selectedRowIndex}`);
28689
- // Use a setTimeout otherwise the swiper cannot slide
28690
- setTimeout(() => {
28691
- const swiper = this.getSwiper(swiperElement);
28692
- swiper.slideTo(this._selectedRowIndex, 0);
28693
- });
28694
- }
28695
- }
28696
- async zoom(swiperElement, zoomIn) {
28697
- if (this.debug)
28698
- console.debug('[image-gallery] zoom ' + (zoomIn ? 'in' : 'out'), swiperElement);
28699
- const swiper = this.getSwiper(swiperElement);
28700
- const zoom = swiper.zoom;
28701
- if (zoomIn)
28702
- zoom.in();
28703
- else
28704
- zoom.out();
28705
- }
28706
- async deleteFromModal(event, swiperElement) {
28707
- const swiper = this.getSwiper(swiperElement);
28708
- const currentRow = this.rows.find((row, index) => index === swiper.activeIndex);
28709
- if (!currentRow)
28710
- return false; // Row not found
28711
- const deleted = await this.delete(event, currentRow);
28712
- if (this.rows.length === 0) {
28713
- await this.zoomModal.dismiss();
28714
- }
28715
- else {
28716
- // Reset the zoom
28717
- swiper.zoom.out();
28718
- swiper.update();
28719
- this.markForCheck();
28720
- }
28721
- return deleted;
28722
- }
28723
- async slidePrev(swiperElement) {
28724
- const swiper = this.getSwiper(swiperElement);
28725
- const activeIndex = swiper.activeIndex;
28726
- if (activeIndex === 0) {
28727
- // Loop to last
28728
- await swiper.slideTo(this.rows.length - 1, 0 /*no transition, to avoid a "revert" effect*/);
28729
- }
28730
- else {
28731
- await swiper.slidePrev();
28732
- }
28733
- }
28734
- async slideNext(swiperElement) {
28735
- const swiper = this.getSwiper(swiperElement);
28736
- const activeIndex = swiper.activeIndex;
28737
- if (activeIndex === this.rows.length - 1) {
28738
- // Loop to first
28739
- await swiper.slideTo(0, 0 /*no transition, to avoid a "revert" effect*/);
29379
+ /**
29380
+ * Handles a delete request coming from the slideshow component (either inline or in the modal):
29381
+ * runs the same confirmation/deletion flow as a card's delete button, then resolves the promise
29382
+ * so the slideshow knows whether to reset its zoom or close.
29383
+ */
29384
+ async onSlideshowDeleteRequested(promiseEvent) {
29385
+ try {
29386
+ const deleted = await this.delete(promiseEvent, promiseEvent.detail.row);
29387
+ promiseEvent.detail.success(deleted);
29388
+ if (deleted && this.rows.length === 0 && this.zoomPresentation === 'inline') {
29389
+ this.closeInlineSlideshow();
29390
+ }
28740
29391
  }
28741
- else {
28742
- await swiper.slideNext();
29392
+ catch (err) {
29393
+ promiseEvent.detail.error(err);
28743
29394
  }
28744
29395
  }
28745
29396
  async canDeleteRows(rows, opts) {
@@ -28778,11 +29429,11 @@ class AppImageGalleryComponent {
28778
29429
  this.cd.markForCheck();
28779
29430
  }
28780
29431
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AppImageGalleryComponent, deps: [{ token: ImageService }, { token: i2$1.Platform }, { token: i2$1.AlertController }, { token: i1$1.TranslateService }, { token: i0.ChangeDetectorRef }, { token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Component });
28781
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: AppImageGalleryComponent, selector: "app-image-gallery", inputs: { cardColor: "cardColor", debug: ["debug", "debug", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], readOnly: ["readOnly", "readOnly", booleanAttribute], mobile: ["mobile", "mobile", booleanAttribute], mode: "mode", confirmBeforeDelete: "confirmBeforeDelete", showToolbar: ["showToolbar", "showToolbar", booleanAttribute], showFabButton: ["showFabButton", "showFabButton", booleanAttribute], showTitle: ["showTitle", "showTitle", booleanAttribute], showAddToolbarButton: ["showAddToolbarButton", "showAddToolbarButton", booleanAttribute], showAddTextButton: ["showAddTextButton", "showAddTextButton", booleanAttribute], showAddCardButton: ["showAddCardButton", "showAddCardButton", booleanAttribute], showCardToolbar: ["showCardToolbar", "showCardToolbar", booleanAttribute], addButtonColor: "addButtonColor", addButtonText: "addButtonText", cardTemplate: "cardTemplate", imageSizes: "imageSizes", imageSizeQueryParam: "imageSizeQueryParam", dataSource: "dataSource" }, outputs: { onBeforeDeleteRows: "onBeforeDeleteRows", onAfterAddRows: "onAfterAddRows", onAfterEditRow: "onAfterEditRow", click: "click" }, viewQueries: [{ propertyName: "zoomModal", first: true, predicate: ["zoomModal"], descendants: true }, { propertyName: "titlePopover", first: true, predicate: ["titlePopover"], descendants: true }, { propertyName: "swiperRef", first: true, predicate: ["modalSwiper"], descendants: true }], ngImport: i0, template: "<ion-toolbar color=\"light\" *ngIf=\"showToolbar\">\n <ng-content select=\"ion-buttons[slot=start]\"></ng-content>\n\n <!-- Add -->\n @if (!readOnly && showAddToolbarButton && enabled) {\n <button\n mat-icon-button\n [title]=\"!showTooltip ? ('COMMON.BTN_ADD' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_ADD' | translate) : ''\"\n (click)=\"add()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n\n <ng-content select=\"ion-buttons[slot=end]\"></ng-content>\n\n <ion-buttons slot=\"end\">\n <!-- Toggle view mode -->\n <ion-segment (ionChange)=\"toggleViewMode($event)\" color=\"accent\" [value]=\"_colSize\">\n <ion-segment-button [value]=\"4\">\n <mat-icon>view_module</mat-icon>\n </ion-segment-button>\n <ion-segment-button [value]=\"12\">\n <mat-icon>view_list</mat-icon>\n </ion-segment-button>\n </ion-segment>\n </ion-buttons>\n</ion-toolbar>\n\n<div class=\"gallery-container ion-padding-bottom {{ this.mode }}\">\n <div *ngIf=\"mobile && zoomActive\" class=\"backdrop\" [style.opacity]=\"zoomScale\"></div>\n\n <ion-grid class=\"ion-no-padding\">\n <ion-row>\n <ion-col *rxFor=\"let row of _data$; index as index; trackBy: trackByFn\" [size]=\"_colSize\">\n @let image = row | propertyGet: 'currentData';\n @if (cardTemplate) {\n <ng-container\n *ngTemplateOutlet=\"cardTemplate; context: { $implicit: row, mode: mode, image: image }\"\n ></ng-container>\n } @else {\n <ion-card\n #card\n class=\"image-card\"\n [class.zoom-hover]=\"!mobile\"\n [color]=\"cardColor\"\n (click)=\"clickRow(row, index)\"\n tappable\n @fadeInAnimation\n >\n <figure class=\"card-thumbnail\">\n @if (image.url) {\n <ion-img [src]=\"image.url | appendQueryParams: getImageSizeQueryParams()\" />\n } @else {\n <ion-img [src]=\"image.dataUrl\" />\n }\n </figure>\n\n <!-- toolbar for title (when not hover) -->\n @if (showTitle && image.title) {\n <ion-toolbar color=\"transparent\" class=\"title\">\n <ion-label class=\"card-title\">{{ image.title }}</ion-label>\n </ion-toolbar>\n }\n\n <!-- action toolbar shown when hover image -->\n @if (!readOnly && !mobile && showCardToolbar) {\n <ion-toolbar #cardToolbar color=\"light\" class=\"buttons\">\n <!-- edit title button -->\n @if (showTitle) {\n <button\n mat-button\n slot=\"start\"\n [disabled]=\"disabled\"\n (click)=\"editTitle($event, row, cardToolbar)\"\n (focusin)=\"focusCardToolbar(cardToolbar)\"\n >\n <mat-icon>edit</mat-icon>\n <mat-label>\n <span *ngIf=\"row.currentData.title | isNotNilOrBlank; else editLabel\" translate>\n IMAGE.GALLERY.BTN_EDIT_TITLE\n </span>\n <ng-template #editLabel>\n <span translate>IMAGE.GALLERY.BTN_ADD_TITLE</span>\n </ng-template>\n </mat-label>\n </button>\n }\n\n <div class=\"flex-spacer\"></div>\n\n <!-- delete button -->\n <button\n mat-icon-button\n slot=\"end\"\n class=\"ion-float-end\"\n [title]=\"!showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [disabled]=\"disabled\"\n (click)=\"delete($event, row)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </ion-toolbar>\n }\n </ion-card>\n }\n </ion-col>\n\n <!-- add button -->\n @if ((showAddTextButton || showAddCardButton) && !readOnly) {\n <ion-col [size]=\"_colSize\" @fadeInAnimation>\n @if (showAddTextButton) {\n <ion-button (click)=\"add($event)\" [disabled]=\"disabled\" [color]=\"addButtonColor\">\n <mat-label translate>{{ addButtonText }}</mat-label>\n <ion-icon slot=\"end\" name=\"camera\"></ion-icon>\n </ion-button>\n } @else if (showAddCardButton) {\n <ion-card\n class=\"image-card-button\"\n color=\"light\"\n (click)=\"add($event)\"\n [disabled]=\"disabled\"\n [title]=\"!showTooltip ? addButtonText : ''\"\n [matTooltip]=\"showTooltip ? addButtonText : ''\"\n tappable\n >\n <ion-card-content>\n <div class=\"icon-container\">\n <ion-icon name=\"camera\" [color]=\"addButtonColor\"></ion-icon>\n <ion-icon name=\"add\" [color]=\"addButtonColor\" class=\"icon-secondary\"></ion-icon>\n </div>\n </ion-card-content>\n </ion-card>\n }\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n</div>\n\n<!-- FAB button: add -->\n<ion-fab\n slot=\"fixed\"\n vertical=\"bottom\"\n horizontal=\"end\"\n *ngIf=\"showFabButton && !readOnly\"\n [class.cdk-visually-hidden]=\"disabled\"\n @fadeInOutAnimation\n>\n <ion-fab-button color=\"tertiary\" (click)=\"add($event)\">\n <ion-icon name=\"camera\"></ion-icon>\n <ion-icon name=\"add\" class=\"icon-secondary\" style=\"left: 33px; top: 9px; font-size: 20px\"></ion-icon>\n </ion-fab-button>\n</ion-fab>\n\n<!-- Zoom modal -->\n<ion-modal #zoomModal class=\"stack-modal zoom-modal\" [class.modal-fullscreen]=\"mobile\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-content class=\"ion-no-padding ion-no-margin\">\n @let count = rows?.length || 0;\n <!-- Counter -->\n <div class=\"top-left\">\n <ion-label color=\"light\">{{ activeSlideIndex + 1 }} / {{ count }}</ion-label>\n </div>\n\n <!-- zoom button -->\n <div class=\"top-right\">\n @if (!mobile) {\n <ion-fab-button size=\"small\" (click)=\"zoom(modalSwiper, true)\" color=\"light\">\n <ion-icon name=\"search\"></ion-icon>\n <ion-icon class=\"icon-secondary\" name=\"add\"></ion-icon>\n </ion-fab-button>\n <ion-fab-button size=\"small\" (click)=\"zoom(modalSwiper, false)\" color=\"light\">\n <ion-icon name=\"search\"></ion-icon>\n <ion-icon class=\"icon-secondary\" name=\"remove\"></ion-icon>\n </ion-fab-button>\n }\n </div>\n\n <!-- navigation side buttons next/back -->\n <ng-container *ngIf=\"!mobile && (rows | isArrayLength: { greaterThan: 1 })\">\n <div class=\"side-buttons prev\" (click)=\"slidePrev(modalSwiper)\">\n <ion-icon name=\"chevron-back\"></ion-icon>\n </div>\n <div class=\"side-buttons next\" (click)=\"slideNext(modalSwiper)\">\n <ion-icon name=\"chevron-forward\"></ion-icon>\n </div>\n </ng-container>\n\n <!-- image slides -->\n <swiper-container\n #modalSwiper\n [modules]=\"swiperModules\"\n [zoom]=\"true\"\n (swiperslidechange)=\"onSlideChange(modalSwiper)\"\n (swiperinit)=\"initModalSlides(modalSwiper)\"\n >\n @for (row of rows; track row.id; let last = $last) {\n <swiper-slide>\n @let image = row | propertyGet: 'currentData';\n <div class=\"swiper-zoom-container\">\n @if (image.url) {\n <ion-img [src]=\"image.url | appendQueryParams: getImageSizeQueryParams('modal')\" />\n } @else {\n <img [src]=\"image.dataUrl\" [alt]=\"image.title\" />\n }\n </div>\n </swiper-slide>\n }\n </swiper-container>\n </ion-content>\n\n <ion-footer>\n <ion-toolbar color=\"light\">\n <ion-row>\n <ion-col size=\"4\" class=\"ion-text-start\">\n <ion-button\n *ngIf=\"!readOnly\"\n (click)=\"deleteFromModal($event, modalSwiper)\"\n [disabled]=\"disabled\"\n fill=\"clear\"\n color=\"danger\"\n >\n <mat-icon slot=\"start\">delete</mat-icon>\n {{ 'COMMON.BTN_DELETE' | translate }}\n </ion-button>\n </ion-col>\n <ion-col size=\"4\"></ion-col>\n <ion-col size=\"4\" class=\"ion-text-end\">\n <ion-button (click)=\"zoomModal.dismiss()\" color=\"tertiary\" fill=\"clear\" *ngIf=\"mobile; else desktop\">\n <ion-icon slot=\"start\" name=\"close\"></ion-icon>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n <ng-template #desktop>\n <ion-button (click)=\"zoomModal.dismiss()\" color=\"tertiary\">\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ng-template>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n </ion-footer>\n </ng-template>\n</ion-modal>\n\n<ion-popover #titlePopover>\n <ng-template>\n <ion-content class=\"ion-padding\" cdkTrapFocus [cdkTrapFocusAutoCapture]=\"true\">\n <form [formGroup]=\"_titleForm\">\n <mat-form-field floatLabel=\"auto\">\n <mat-label translate>IMAGE.GALLERY.TITLE</mat-label>\n <input\n matInput\n formControlName=\"title\"\n [readonly]=\"disabled\"\n [appAutofocus]=\"_titleAutofocus\"\n (keydown.control.enter)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n />\n </mat-form-field>\n </form>\n </ion-content>\n\n <ion-footer>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"titlePopover.dismiss(null, 'CANCEL')\">\n <ion-label translate>COMMON.BTN_CANCEL</ion-label>\n </ion-button>\n <ion-button\n [fill]=\"disabled ? 'clear' : 'solid'\"\n (click)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n (keyup.enter)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n color=\"tertiary\"\n [disabled]=\"disabled\"\n [title]=\"!showTooltip ? ('COMMON.BTN_VALIDATE_WITH_SHORTCUT_HELP' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_VALIDATE_WITH_SHORTCUT_HELP' | translate) : ''\"\n >\n <ion-label translate>COMMON.BTN_VALIDATE</ion-label>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n </ion-footer>\n </ng-template>\n</ion-popover>\n", styles: [":host{--image-margin-inline: 10px;--image-margin-safe-area: var(--image-margin-inline, 0px) * 2 - 3px;--segment-max-width: 150px}ion-toolbar{position:sticky;top:0}ion-toolbar ion-segment{max-width:var(--segment-max-width)}mat-toolbar,.mat-toolbar-single-row{padding:0;height:42px}ion-card{margin-left:unset;margin-right:unset;margin-inline:var(--image-margin-inline, 10px)}.image-slide{overflow:visible}.image-card{background-color:var(--ion-color-base);z-index:9;--img-scale: 1}.image-card .card-thumbnail{position:relative;overflow:hidden;display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:100%;height:100%;max-height:var(--img-max-height, calc(100% - var(--image-margin-safe-area, 0px)))}.image-card .card-thumbnail>ion-img{min-height:100%;height:100%;object-fit:cover;object-position:center;transition:scale .2s ease-in-out;scale:var(--img-scale)}.image-card .card-thumbnail>ion-img img{max-height:var(--img-max-height)}.image-card.zoom-hover:hover{--img-scale: 1.2}.image-card ion-toolbar{--top-offset: calc(-1 * var(--ion-toolbar-height));position:absolute}.image-card ion-toolbar.title{--padding-start: 16px;--padding-end: 16px;--ion-color-base: rgba(0, 0, 0, .45) !important;position:absolute;top:calc(100% + var(--top-offset));transition:opacity .2s ease-in-out}.image-card ion-toolbar.title ion-label{color:#fff}.image-card ion-toolbar.buttons{--padding-start: 4px;--padding-end: 4px;--ion-color-base: var(--ion-color-light) !important;position:absolute;top:100%;transition:transform .2s ease-in-out}.image-card:hover ion-toolbar.buttons,.image-card ion-toolbar.buttons.focused{transform:translateY(var(--top-offset))}.image-card-button{height:calc(100% - var(--image-margin-safe-area, 0px));max-height:max(min(30vh,200px),var(--img-max-height, 0px));transition:.2s ease-in-out;max-width:250px}.image-card-button ion-card-content{height:100%}.image-card-button .icon-container{height:auto;position:relative;top:calc(50% - 75px);display:flex;align-items:center;justify-content:center}.image-card-button ion-icon:first-of-type{font-size:150px;height:150px;position:relative;top:0}.image-card-button ion-icon.icon-secondary{font-size:50px;position:absolute;top:0;left:max(min(50% + 25px,100% - 50px),0px)}.image-card-button ion-button{float:right}.image-card-button:hover{--ion-color-base: var(--ion-color-shade) !important}.image-card-button:hover ion-icon{--ion-color-base: var(--ion-color-shade) !important}.backdrop{height:100vh;width:100vw;background:#000;position:absolute;z-index:10}.zoom-modal{-webkit-user-select:none;user-select:none}.zoom-modal .top-left{z-index:14;position:absolute;display:inline-grid;left:var(--ion-padding-start, 8px);top:var(--ion-padding-start, 8px)}.zoom-modal .top-right{z-index:14;position:absolute;display:inline-grid;right:var(--ion-padding-start, 8px);top:var(--ion-padding-start, 8px)}.zoom-modal .side-buttons{--side-buttons-width: 120px;--side-buttons-margin: calc(var(--side-buttons-width) / 3);z-index:13;position:absolute;top:0;bottom:0;width:var(--side-buttons-width)}.zoom-modal .side-buttons ion-icon{top:50%;color:#fff;font-size:2.5rem;position:absolute;opacity:0;transition-duration:.15s;transition-timing-function:ease-in}.zoom-modal .side-buttons:hover{cursor:pointer}.zoom-modal .side-buttons:hover ion-icon{opacity:1}.zoom-modal .side-buttons.prev{left:0}.zoom-modal .side-buttons.prev ion-icon{left:var(--side-buttons-margin)}.zoom-modal .side-buttons.next{right:0}.zoom-modal .side-buttons.next ion-icon{right:var(--side-buttons-margin)}.zoom-modal swiper-container{height:100%;background:#000}.zoom-modal ion-fab-button .icon-secondary{top:11px;left:6px;height:14px}\n"], dependencies: [{ kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonFab, selector: "ion-fab", inputs: ["activated", "edge", "horizontal", "vertical"] }, { kind: "component", type: i2$1.IonFabButton, selector: "ion-fab-button", inputs: ["activated", "closeIcon", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "show", "size", "target", "translucent", "type"] }, { kind: "component", type: i2$1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonSegment, selector: "ion-segment", inputs: ["color", "disabled", "mode", "scrollable", "selectOnFocus", "swipeGesture", "value"] }, { kind: "component", type: i2$1.IonSegmentButton, selector: "ion-segment-button", inputs: ["disabled", "layout", "mode", "type", "value"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonModal, selector: "ion-modal" }, { kind: "component", type: i2$1.IonPopover, selector: "ion-popover" }, { kind: "directive", type: i2$1.SelectValueAccessor, selector: "ion-select, ion-radio-group, ion-segment, ion-datetime" }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i11$1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i11$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "directive", type: i1$6.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: i1$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: AutofocusDirective, selector: "[autofocus], input[appAutofocus]", inputs: ["appAutofocus", "autofocusDelay"] }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: i13$3.RxFor, selector: "[rxFor][rxForOf]", inputs: ["rxForOf", "rxForTemplate", "rxForStrategy", "rxForParent", "rxForPatchZone", "rxForTrackBy", "rxForRenderCallback"] }, { kind: "pipe", type: PropertyGetPipe, name: "propertyGet" }, { kind: "pipe", type: ArrayLengthPipe, name: "isArrayLength" }, { kind: "pipe", type: IsNotNilOrBlankPipe, name: "isNotNilOrBlank" }, { kind: "pipe", type: AppendQueryParamsPipePipe, name: "appendQueryParams" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], animations: [fadeInAnimation, fadeInOutAnimation], changeDetection: i0.ChangeDetectionStrategy.OnPush });
29432
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: AppImageGalleryComponent, selector: "app-image-gallery", inputs: { cardColor: "cardColor", debug: ["debug", "debug", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], readOnly: ["readOnly", "readOnly", booleanAttribute], mobile: ["mobile", "mobile", booleanAttribute], mode: "mode", confirmBeforeDelete: "confirmBeforeDelete", showToolbar: ["showToolbar", "showToolbar", booleanAttribute], showFabButton: ["showFabButton", "showFabButton", booleanAttribute], showTitle: ["showTitle", "showTitle", booleanAttribute], showAddToolbarButton: ["showAddToolbarButton", "showAddToolbarButton", booleanAttribute], showAddTextButton: ["showAddTextButton", "showAddTextButton", booleanAttribute], showAddCardButton: ["showAddCardButton", "showAddCardButton", booleanAttribute], showCardToolbar: ["showCardToolbar", "showCardToolbar", booleanAttribute], addButtonColor: "addButtonColor", addButtonText: "addButtonText", cardTemplate: "cardTemplate", imageSizes: "imageSizes", imageSizeQueryParam: "imageSizeQueryParam", enableMouseZoom: ["enableMouseZoom", "enableMouseZoom", booleanAttribute], wheelZoomStep: "wheelZoomStep", maxZoomRatio: "maxZoomRatio", enableRotation: ["enableRotation", "enableRotation", booleanAttribute], enableCrop: ["enableCrop", "enableCrop", booleanAttribute], imageExportFormat: "imageExportFormat", imageExportQuality: "imageExportQuality", zoomPresentation: "zoomPresentation", inlineZoomHeight: "inlineZoomHeight", dataSource: "dataSource" }, outputs: { onBeforeDeleteRows: "onBeforeDeleteRows", onAfterAddRows: "onAfterAddRows", onAfterEditRow: "onAfterEditRow", onAfterEditImage: "onAfterEditImage", click: "click" }, viewQueries: [{ propertyName: "zoomModal", first: true, predicate: ["zoomModal"], descendants: true }, { propertyName: "titlePopover", first: true, predicate: ["titlePopover"], descendants: true }], ngImport: i0, template: "<ion-toolbar color=\"light\" class=\"gallery-toolbar\" *ngIf=\"showToolbar && !_inlineSlideshowActive\">\n <ng-content select=\"ion-buttons[slot=start]\"></ng-content>\n\n <!-- Add -->\n @if (!readOnly && showAddToolbarButton && enabled) {\n <button\n mat-icon-button\n [title]=\"!showTooltip ? ('COMMON.BTN_ADD' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_ADD' | translate) : ''\"\n (click)=\"add()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n\n <ng-content select=\"ion-buttons[slot=end]\"></ng-content>\n\n <ion-buttons slot=\"end\">\n <!-- Toggle view mode -->\n <ion-segment (ionChange)=\"toggleViewMode($event)\" color=\"accent\" [value]=\"_colSize\">\n <ion-segment-button [value]=\"4\">\n <mat-icon>view_module</mat-icon>\n </ion-segment-button>\n <ion-segment-button [value]=\"12\">\n <mat-icon>view_list</mat-icon>\n </ion-segment-button>\n </ion-segment>\n </ion-buttons>\n</ion-toolbar>\n\n<div\n class=\"gallery-container ion-padding-bottom {{ mode }}\"\n [class.inline-zoom-active]=\"_inlineSlideshowActive\"\n [style.--inline-zoom-height]=\"inlineZoomHeight\"\n>\n <div *ngIf=\"mobile && zoomActive\" class=\"backdrop\" [style.opacity]=\"zoomScale\"></div>\n\n <ion-grid class=\"ion-no-padding\" [class.cdk-visually-hidden]=\"_inlineSlideshowActive\">\n <ion-row>\n <ion-col *rxFor=\"let row of _data$; index as index; trackBy: trackByFn\" [size]=\"_colSize\">\n @let image = row | propertyGet: 'currentData';\n @if (cardTemplate) {\n <ng-container\n *ngTemplateOutlet=\"cardTemplate; context: { $implicit: row, mode: mode, image: image }\"\n ></ng-container>\n } @else {\n <ion-card\n #card\n class=\"image-card\"\n [class.zoom-hover]=\"!mobile\"\n [color]=\"cardColor\"\n (click)=\"clickRow(row, index)\"\n tappable\n @fadeInAnimation\n >\n <figure class=\"card-thumbnail\">\n @if (image.url) {\n <ion-img [src]=\"image.url | appendQueryParams: getImageSizeQueryParams()\" />\n } @else {\n <ion-img [src]=\"image.dataUrl\" />\n }\n </figure>\n\n <!-- toolbar for title (when not hover) -->\n @if (showTitle && image.title) {\n <ion-toolbar color=\"transparent\" class=\"title\">\n <ion-label class=\"card-title\">{{ image.title }}</ion-label>\n </ion-toolbar>\n }\n\n <!-- action toolbar shown when hover image -->\n @if (!readOnly && !mobile && showCardToolbar) {\n <ion-toolbar #cardToolbar color=\"light\" class=\"buttons\">\n <!-- edit title button -->\n @if (showTitle) {\n <button\n mat-button\n slot=\"start\"\n [disabled]=\"disabled\"\n (click)=\"editTitle($event, row, cardToolbar)\"\n (focusin)=\"focusCardToolbar(cardToolbar)\"\n >\n <mat-icon>edit</mat-icon>\n <mat-label>\n <span *ngIf=\"row.currentData.title | isNotNilOrBlank; else editLabel\" translate>\n IMAGE.GALLERY.BTN_EDIT_TITLE\n </span>\n <ng-template #editLabel>\n <span translate>IMAGE.GALLERY.BTN_ADD_TITLE</span>\n </ng-template>\n </mat-label>\n </button>\n }\n\n <div class=\"flex-spacer\"></div>\n\n <!-- delete button -->\n <button\n mat-icon-button\n slot=\"end\"\n class=\"ion-float-end\"\n [title]=\"!showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [disabled]=\"disabled\"\n (click)=\"delete($event, row)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </ion-toolbar>\n }\n </ion-card>\n }\n </ion-col>\n\n <!-- add button -->\n @if ((showAddTextButton || showAddCardButton) && !readOnly) {\n <ion-col [size]=\"_colSize\" @fadeInAnimation>\n @if (showAddTextButton) {\n <ion-button (click)=\"add($event)\" [disabled]=\"disabled\" [color]=\"addButtonColor\">\n <mat-label translate>{{ addButtonText }}</mat-label>\n <ion-icon slot=\"end\" name=\"camera\"></ion-icon>\n </ion-button>\n } @else if (showAddCardButton) {\n <ion-card\n class=\"image-card-button\"\n color=\"light\"\n (click)=\"add($event)\"\n [disabled]=\"disabled\"\n [title]=\"!showTooltip ? addButtonText : ''\"\n [matTooltip]=\"showTooltip ? addButtonText : ''\"\n tappable\n >\n <ion-card-content>\n <div class=\"icon-container\">\n <ion-icon name=\"camera\" [color]=\"addButtonColor\"></ion-icon>\n <ion-icon name=\"add\" [color]=\"addButtonColor\" class=\"icon-secondary\"></ion-icon>\n </div>\n </ion-card-content>\n </ion-card>\n }\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n\n <!-- Inline slideshow: shown as an overlay directly within the gallery, instead of a full-screen modal.\n The rest of the page (outside the gallery) stays fully visible and usable, unlike the modal's backdrop. -->\n @if (zoomPresentation === 'inline' && _inlineSlideshowActive) {\n <app-image-gallery-slideshow\n class=\"inline-overlay\"\n @fadeInOutAnimation\n [rows]=\"rows\"\n [selectedIndex]=\"_selectedRowIndex\"\n [mobile]=\"mobile\"\n [readOnly]=\"readOnly\"\n [disabled]=\"disabled\"\n [debug]=\"debug\"\n [mouseZoomEnabled]=\"_mouseZoomEnabled\"\n [wheelZoomStep]=\"wheelZoomStep\"\n [maxZoomRatio]=\"maxZoomRatio\"\n [rotationEnabled]=\"_rotationEnabled\"\n [cropEnabled]=\"_cropEnabled\"\n [imageExportFormat]=\"imageExportFormat\"\n [imageExportQuality]=\"imageExportQuality\"\n [imageSizeQueryParam]=\"imageSizeQueryParam\"\n [modalImageSize]=\"imageSizes?.modal\"\n (deleteRequested)=\"onSlideshowDeleteRequested($event)\"\n (closeRequested)=\"closeInlineSlideshow()\"\n (afterEditImage)=\"onAfterEditImage.emit($event)\"\n (afterEditRow)=\"onAfterEditRow.emit($event)\"\n ></app-image-gallery-slideshow>\n }\n</div>\n\n<!-- FAB button: add -->\n<ion-fab\n slot=\"fixed\"\n vertical=\"bottom\"\n horizontal=\"end\"\n *ngIf=\"showFabButton && !readOnly\"\n [class.cdk-visually-hidden]=\"disabled\"\n @fadeInOutAnimation\n>\n <ion-fab-button color=\"tertiary\" (click)=\"add($event)\">\n <ion-icon name=\"camera\"></ion-icon>\n <ion-icon name=\"add\" class=\"icon-secondary\" style=\"left: 33px; top: 9px; font-size: 20px\"></ion-icon>\n </ion-fab-button>\n</ion-fab>\n\n<!-- Zoom modal -->\n@if (zoomPresentation === 'modal') {\n <ion-modal #zoomModal class=\"stack-modal zoom-modal\" [class.modal-fullscreen]=\"mobile\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <app-image-gallery-slideshow\n [rows]=\"rows\"\n [selectedIndex]=\"_selectedRowIndex\"\n [mobile]=\"mobile\"\n [readOnly]=\"readOnly\"\n [disabled]=\"disabled\"\n [debug]=\"debug\"\n [mouseZoomEnabled]=\"_mouseZoomEnabled\"\n [wheelZoomStep]=\"wheelZoomStep\"\n [maxZoomRatio]=\"maxZoomRatio\"\n [rotationEnabled]=\"_rotationEnabled\"\n [cropEnabled]=\"_cropEnabled\"\n [imageExportFormat]=\"imageExportFormat\"\n [imageExportQuality]=\"imageExportQuality\"\n [imageSizeQueryParam]=\"imageSizeQueryParam\"\n [modalImageSize]=\"imageSizes?.modal\"\n (deleteRequested)=\"onSlideshowDeleteRequested($event)\"\n (closeRequested)=\"zoomModal.dismiss()\"\n (afterEditImage)=\"onAfterEditImage.emit($event)\"\n (afterEditRow)=\"onAfterEditRow.emit($event)\"\n ></app-image-gallery-slideshow>\n </ng-template>\n </ion-modal>\n}\n\n<ion-popover #titlePopover>\n <ng-template>\n <ion-content class=\"ion-padding\" cdkTrapFocus [cdkTrapFocusAutoCapture]=\"true\">\n <form [formGroup]=\"_titleForm\">\n <mat-form-field floatLabel=\"auto\">\n <mat-label translate>IMAGE.GALLERY.TITLE</mat-label>\n <input\n matInput\n formControlName=\"title\"\n [readonly]=\"disabled\"\n [appAutofocus]=\"_titleAutofocus\"\n (keydown.control.enter)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n />\n </mat-form-field>\n </form>\n </ion-content>\n\n <ion-footer>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"titlePopover.dismiss(null, 'CANCEL')\">\n <ion-label translate>COMMON.BTN_CANCEL</ion-label>\n </ion-button>\n <ion-button\n [fill]=\"disabled ? 'clear' : 'solid'\"\n (click)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n (keyup.enter)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n color=\"tertiary\"\n [disabled]=\"disabled\"\n [title]=\"!showTooltip ? ('COMMON.BTN_VALIDATE_WITH_SHORTCUT_HELP' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_VALIDATE_WITH_SHORTCUT_HELP' | translate) : ''\"\n >\n <ion-label translate>COMMON.BTN_VALIDATE</ion-label>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n </ion-footer>\n </ng-template>\n</ion-popover>\n", styles: [":host{--image-margin-inline: 10px;--image-margin-safe-area: var(--image-margin-inline, 0px) * 2 - 3px;--segment-max-width: 150px}ion-toolbar{position:sticky;top:0}ion-toolbar ion-segment{max-width:var(--segment-max-width)}mat-toolbar,.mat-toolbar-single-row{padding:0;height:42px}ion-card{margin-left:unset;margin-right:unset;margin-inline:var(--image-margin-inline, 10px)}.image-slide{overflow:visible}.image-card{background-color:var(--ion-color-base);z-index:9;--img-scale: 1}.image-card .card-thumbnail{position:relative;overflow:hidden;display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:100%;height:100%;max-height:var(--img-max-height, calc(100% - var(--image-margin-safe-area, 0px)))}.image-card .card-thumbnail>ion-img{min-height:100%;height:100%;object-fit:cover;object-position:center;transition:scale .2s ease-in-out;scale:var(--img-scale)}.image-card .card-thumbnail>ion-img img{max-height:var(--img-max-height)}.image-card.zoom-hover:hover{--img-scale: 1.2}.image-card ion-toolbar{--top-offset: calc(-1 * var(--ion-toolbar-height));position:absolute}.image-card ion-toolbar.title{--padding-start: 16px;--padding-end: 16px;--ion-color-base: rgba(0, 0, 0, .45) !important;position:absolute;top:calc(100% + var(--top-offset));transition:opacity .2s ease-in-out}.image-card ion-toolbar.title ion-label{color:#fff}.image-card ion-toolbar.buttons{--padding-start: 4px;--padding-end: 4px;--ion-color-base: var(--ion-color-light) !important;position:absolute;top:100%;transition:transform .2s ease-in-out}.image-card:hover ion-toolbar.buttons,.image-card ion-toolbar.buttons.focused{transform:translateY(var(--top-offset))}.image-card-button{height:calc(100% - var(--image-margin-safe-area, 0px));max-height:max(min(30vh,200px),var(--img-max-height, 0px));transition:.2s ease-in-out;max-width:250px}.image-card-button ion-card-content{height:100%}.image-card-button .icon-container{height:auto;position:relative;top:calc(50% - 75px);display:flex;align-items:center;justify-content:center}.image-card-button ion-icon:first-of-type{font-size:150px;height:150px;position:relative;top:0}.image-card-button ion-icon.icon-secondary{font-size:50px;position:absolute;top:0;left:max(min(50% + 25px,100% - 50px),0px)}.image-card-button ion-button{float:right}.image-card-button:hover{--ion-color-base: var(--ion-color-shade) !important}.image-card-button:hover ion-icon{--ion-color-base: var(--ion-color-shade) !important}.backdrop{height:100vh;width:100vw;background:#000;position:absolute;z-index:10}.gallery-container{position:relative}.gallery-container.inline-zoom-active{min-height:var(--inline-zoom-height, 60vh)}.inline-overlay{position:absolute;inset:0;z-index:20;background:#000}\n"], dependencies: [{ kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonFab, selector: "ion-fab", inputs: ["activated", "edge", "horizontal", "vertical"] }, { kind: "component", type: i2$1.IonFabButton, selector: "ion-fab-button", inputs: ["activated", "closeIcon", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "show", "size", "target", "translucent", "type"] }, { kind: "component", type: i2$1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonSegment, selector: "ion-segment", inputs: ["color", "disabled", "mode", "scrollable", "selectOnFocus", "swipeGesture", "value"] }, { kind: "component", type: i2$1.IonSegmentButton, selector: "ion-segment-button", inputs: ["disabled", "layout", "mode", "type", "value"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonModal, selector: "ion-modal" }, { kind: "component", type: i2$1.IonPopover, selector: "ion-popover" }, { kind: "directive", type: i2$1.SelectValueAccessor, selector: "ion-select, ion-radio-group, ion-segment, ion-datetime" }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i11$1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i11$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "directive", type: i1$6.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: i1$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: AutofocusDirective, selector: "[autofocus], input[appAutofocus]", inputs: ["appAutofocus", "autofocusDelay"] }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: i13$3.RxFor, selector: "[rxFor][rxForOf]", inputs: ["rxForOf", "rxForTemplate", "rxForStrategy", "rxForParent", "rxForPatchZone", "rxForTrackBy", "rxForRenderCallback"] }, { kind: "component", type: AppImageGallerySlideshowComponent, selector: "app-image-gallery-slideshow", inputs: ["rows", "selectedIndex", "mobile", "readOnly", "disabled", "debug", "mouseZoomEnabled", "wheelZoomStep", "maxZoomRatio", "rotationEnabled", "cropEnabled", "imageExportFormat", "imageExportQuality", "imageSizeQueryParam", "modalImageSize"], outputs: ["deleteRequested", "closeRequested", "afterEditImage", "afterEditRow"] }, { kind: "pipe", type: PropertyGetPipe, name: "propertyGet" }, { kind: "pipe", type: IsNotNilOrBlankPipe, name: "isNotNilOrBlank" }, { kind: "pipe", type: AppendQueryParamsPipePipe, name: "appendQueryParams" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], animations: [fadeInAnimation, fadeInOutAnimation], changeDetection: i0.ChangeDetectionStrategy.OnPush });
28782
29433
  }
28783
29434
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AppImageGalleryComponent, decorators: [{
28784
29435
  type: Component,
28785
- args: [{ selector: 'app-image-gallery', animations: [fadeInAnimation, fadeInOutAnimation], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-toolbar color=\"light\" *ngIf=\"showToolbar\">\n <ng-content select=\"ion-buttons[slot=start]\"></ng-content>\n\n <!-- Add -->\n @if (!readOnly && showAddToolbarButton && enabled) {\n <button\n mat-icon-button\n [title]=\"!showTooltip ? ('COMMON.BTN_ADD' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_ADD' | translate) : ''\"\n (click)=\"add()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n\n <ng-content select=\"ion-buttons[slot=end]\"></ng-content>\n\n <ion-buttons slot=\"end\">\n <!-- Toggle view mode -->\n <ion-segment (ionChange)=\"toggleViewMode($event)\" color=\"accent\" [value]=\"_colSize\">\n <ion-segment-button [value]=\"4\">\n <mat-icon>view_module</mat-icon>\n </ion-segment-button>\n <ion-segment-button [value]=\"12\">\n <mat-icon>view_list</mat-icon>\n </ion-segment-button>\n </ion-segment>\n </ion-buttons>\n</ion-toolbar>\n\n<div class=\"gallery-container ion-padding-bottom {{ this.mode }}\">\n <div *ngIf=\"mobile && zoomActive\" class=\"backdrop\" [style.opacity]=\"zoomScale\"></div>\n\n <ion-grid class=\"ion-no-padding\">\n <ion-row>\n <ion-col *rxFor=\"let row of _data$; index as index; trackBy: trackByFn\" [size]=\"_colSize\">\n @let image = row | propertyGet: 'currentData';\n @if (cardTemplate) {\n <ng-container\n *ngTemplateOutlet=\"cardTemplate; context: { $implicit: row, mode: mode, image: image }\"\n ></ng-container>\n } @else {\n <ion-card\n #card\n class=\"image-card\"\n [class.zoom-hover]=\"!mobile\"\n [color]=\"cardColor\"\n (click)=\"clickRow(row, index)\"\n tappable\n @fadeInAnimation\n >\n <figure class=\"card-thumbnail\">\n @if (image.url) {\n <ion-img [src]=\"image.url | appendQueryParams: getImageSizeQueryParams()\" />\n } @else {\n <ion-img [src]=\"image.dataUrl\" />\n }\n </figure>\n\n <!-- toolbar for title (when not hover) -->\n @if (showTitle && image.title) {\n <ion-toolbar color=\"transparent\" class=\"title\">\n <ion-label class=\"card-title\">{{ image.title }}</ion-label>\n </ion-toolbar>\n }\n\n <!-- action toolbar shown when hover image -->\n @if (!readOnly && !mobile && showCardToolbar) {\n <ion-toolbar #cardToolbar color=\"light\" class=\"buttons\">\n <!-- edit title button -->\n @if (showTitle) {\n <button\n mat-button\n slot=\"start\"\n [disabled]=\"disabled\"\n (click)=\"editTitle($event, row, cardToolbar)\"\n (focusin)=\"focusCardToolbar(cardToolbar)\"\n >\n <mat-icon>edit</mat-icon>\n <mat-label>\n <span *ngIf=\"row.currentData.title | isNotNilOrBlank; else editLabel\" translate>\n IMAGE.GALLERY.BTN_EDIT_TITLE\n </span>\n <ng-template #editLabel>\n <span translate>IMAGE.GALLERY.BTN_ADD_TITLE</span>\n </ng-template>\n </mat-label>\n </button>\n }\n\n <div class=\"flex-spacer\"></div>\n\n <!-- delete button -->\n <button\n mat-icon-button\n slot=\"end\"\n class=\"ion-float-end\"\n [title]=\"!showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [disabled]=\"disabled\"\n (click)=\"delete($event, row)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </ion-toolbar>\n }\n </ion-card>\n }\n </ion-col>\n\n <!-- add button -->\n @if ((showAddTextButton || showAddCardButton) && !readOnly) {\n <ion-col [size]=\"_colSize\" @fadeInAnimation>\n @if (showAddTextButton) {\n <ion-button (click)=\"add($event)\" [disabled]=\"disabled\" [color]=\"addButtonColor\">\n <mat-label translate>{{ addButtonText }}</mat-label>\n <ion-icon slot=\"end\" name=\"camera\"></ion-icon>\n </ion-button>\n } @else if (showAddCardButton) {\n <ion-card\n class=\"image-card-button\"\n color=\"light\"\n (click)=\"add($event)\"\n [disabled]=\"disabled\"\n [title]=\"!showTooltip ? addButtonText : ''\"\n [matTooltip]=\"showTooltip ? addButtonText : ''\"\n tappable\n >\n <ion-card-content>\n <div class=\"icon-container\">\n <ion-icon name=\"camera\" [color]=\"addButtonColor\"></ion-icon>\n <ion-icon name=\"add\" [color]=\"addButtonColor\" class=\"icon-secondary\"></ion-icon>\n </div>\n </ion-card-content>\n </ion-card>\n }\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n</div>\n\n<!-- FAB button: add -->\n<ion-fab\n slot=\"fixed\"\n vertical=\"bottom\"\n horizontal=\"end\"\n *ngIf=\"showFabButton && !readOnly\"\n [class.cdk-visually-hidden]=\"disabled\"\n @fadeInOutAnimation\n>\n <ion-fab-button color=\"tertiary\" (click)=\"add($event)\">\n <ion-icon name=\"camera\"></ion-icon>\n <ion-icon name=\"add\" class=\"icon-secondary\" style=\"left: 33px; top: 9px; font-size: 20px\"></ion-icon>\n </ion-fab-button>\n</ion-fab>\n\n<!-- Zoom modal -->\n<ion-modal #zoomModal class=\"stack-modal zoom-modal\" [class.modal-fullscreen]=\"mobile\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-content class=\"ion-no-padding ion-no-margin\">\n @let count = rows?.length || 0;\n <!-- Counter -->\n <div class=\"top-left\">\n <ion-label color=\"light\">{{ activeSlideIndex + 1 }} / {{ count }}</ion-label>\n </div>\n\n <!-- zoom button -->\n <div class=\"top-right\">\n @if (!mobile) {\n <ion-fab-button size=\"small\" (click)=\"zoom(modalSwiper, true)\" color=\"light\">\n <ion-icon name=\"search\"></ion-icon>\n <ion-icon class=\"icon-secondary\" name=\"add\"></ion-icon>\n </ion-fab-button>\n <ion-fab-button size=\"small\" (click)=\"zoom(modalSwiper, false)\" color=\"light\">\n <ion-icon name=\"search\"></ion-icon>\n <ion-icon class=\"icon-secondary\" name=\"remove\"></ion-icon>\n </ion-fab-button>\n }\n </div>\n\n <!-- navigation side buttons next/back -->\n <ng-container *ngIf=\"!mobile && (rows | isArrayLength: { greaterThan: 1 })\">\n <div class=\"side-buttons prev\" (click)=\"slidePrev(modalSwiper)\">\n <ion-icon name=\"chevron-back\"></ion-icon>\n </div>\n <div class=\"side-buttons next\" (click)=\"slideNext(modalSwiper)\">\n <ion-icon name=\"chevron-forward\"></ion-icon>\n </div>\n </ng-container>\n\n <!-- image slides -->\n <swiper-container\n #modalSwiper\n [modules]=\"swiperModules\"\n [zoom]=\"true\"\n (swiperslidechange)=\"onSlideChange(modalSwiper)\"\n (swiperinit)=\"initModalSlides(modalSwiper)\"\n >\n @for (row of rows; track row.id; let last = $last) {\n <swiper-slide>\n @let image = row | propertyGet: 'currentData';\n <div class=\"swiper-zoom-container\">\n @if (image.url) {\n <ion-img [src]=\"image.url | appendQueryParams: getImageSizeQueryParams('modal')\" />\n } @else {\n <img [src]=\"image.dataUrl\" [alt]=\"image.title\" />\n }\n </div>\n </swiper-slide>\n }\n </swiper-container>\n </ion-content>\n\n <ion-footer>\n <ion-toolbar color=\"light\">\n <ion-row>\n <ion-col size=\"4\" class=\"ion-text-start\">\n <ion-button\n *ngIf=\"!readOnly\"\n (click)=\"deleteFromModal($event, modalSwiper)\"\n [disabled]=\"disabled\"\n fill=\"clear\"\n color=\"danger\"\n >\n <mat-icon slot=\"start\">delete</mat-icon>\n {{ 'COMMON.BTN_DELETE' | translate }}\n </ion-button>\n </ion-col>\n <ion-col size=\"4\"></ion-col>\n <ion-col size=\"4\" class=\"ion-text-end\">\n <ion-button (click)=\"zoomModal.dismiss()\" color=\"tertiary\" fill=\"clear\" *ngIf=\"mobile; else desktop\">\n <ion-icon slot=\"start\" name=\"close\"></ion-icon>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n <ng-template #desktop>\n <ion-button (click)=\"zoomModal.dismiss()\" color=\"tertiary\">\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ng-template>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n </ion-footer>\n </ng-template>\n</ion-modal>\n\n<ion-popover #titlePopover>\n <ng-template>\n <ion-content class=\"ion-padding\" cdkTrapFocus [cdkTrapFocusAutoCapture]=\"true\">\n <form [formGroup]=\"_titleForm\">\n <mat-form-field floatLabel=\"auto\">\n <mat-label translate>IMAGE.GALLERY.TITLE</mat-label>\n <input\n matInput\n formControlName=\"title\"\n [readonly]=\"disabled\"\n [appAutofocus]=\"_titleAutofocus\"\n (keydown.control.enter)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n />\n </mat-form-field>\n </form>\n </ion-content>\n\n <ion-footer>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"titlePopover.dismiss(null, 'CANCEL')\">\n <ion-label translate>COMMON.BTN_CANCEL</ion-label>\n </ion-button>\n <ion-button\n [fill]=\"disabled ? 'clear' : 'solid'\"\n (click)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n (keyup.enter)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n color=\"tertiary\"\n [disabled]=\"disabled\"\n [title]=\"!showTooltip ? ('COMMON.BTN_VALIDATE_WITH_SHORTCUT_HELP' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_VALIDATE_WITH_SHORTCUT_HELP' | translate) : ''\"\n >\n <ion-label translate>COMMON.BTN_VALIDATE</ion-label>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n </ion-footer>\n </ng-template>\n</ion-popover>\n", styles: [":host{--image-margin-inline: 10px;--image-margin-safe-area: var(--image-margin-inline, 0px) * 2 - 3px;--segment-max-width: 150px}ion-toolbar{position:sticky;top:0}ion-toolbar ion-segment{max-width:var(--segment-max-width)}mat-toolbar,.mat-toolbar-single-row{padding:0;height:42px}ion-card{margin-left:unset;margin-right:unset;margin-inline:var(--image-margin-inline, 10px)}.image-slide{overflow:visible}.image-card{background-color:var(--ion-color-base);z-index:9;--img-scale: 1}.image-card .card-thumbnail{position:relative;overflow:hidden;display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:100%;height:100%;max-height:var(--img-max-height, calc(100% - var(--image-margin-safe-area, 0px)))}.image-card .card-thumbnail>ion-img{min-height:100%;height:100%;object-fit:cover;object-position:center;transition:scale .2s ease-in-out;scale:var(--img-scale)}.image-card .card-thumbnail>ion-img img{max-height:var(--img-max-height)}.image-card.zoom-hover:hover{--img-scale: 1.2}.image-card ion-toolbar{--top-offset: calc(-1 * var(--ion-toolbar-height));position:absolute}.image-card ion-toolbar.title{--padding-start: 16px;--padding-end: 16px;--ion-color-base: rgba(0, 0, 0, .45) !important;position:absolute;top:calc(100% + var(--top-offset));transition:opacity .2s ease-in-out}.image-card ion-toolbar.title ion-label{color:#fff}.image-card ion-toolbar.buttons{--padding-start: 4px;--padding-end: 4px;--ion-color-base: var(--ion-color-light) !important;position:absolute;top:100%;transition:transform .2s ease-in-out}.image-card:hover ion-toolbar.buttons,.image-card ion-toolbar.buttons.focused{transform:translateY(var(--top-offset))}.image-card-button{height:calc(100% - var(--image-margin-safe-area, 0px));max-height:max(min(30vh,200px),var(--img-max-height, 0px));transition:.2s ease-in-out;max-width:250px}.image-card-button ion-card-content{height:100%}.image-card-button .icon-container{height:auto;position:relative;top:calc(50% - 75px);display:flex;align-items:center;justify-content:center}.image-card-button ion-icon:first-of-type{font-size:150px;height:150px;position:relative;top:0}.image-card-button ion-icon.icon-secondary{font-size:50px;position:absolute;top:0;left:max(min(50% + 25px,100% - 50px),0px)}.image-card-button ion-button{float:right}.image-card-button:hover{--ion-color-base: var(--ion-color-shade) !important}.image-card-button:hover ion-icon{--ion-color-base: var(--ion-color-shade) !important}.backdrop{height:100vh;width:100vw;background:#000;position:absolute;z-index:10}.zoom-modal{-webkit-user-select:none;user-select:none}.zoom-modal .top-left{z-index:14;position:absolute;display:inline-grid;left:var(--ion-padding-start, 8px);top:var(--ion-padding-start, 8px)}.zoom-modal .top-right{z-index:14;position:absolute;display:inline-grid;right:var(--ion-padding-start, 8px);top:var(--ion-padding-start, 8px)}.zoom-modal .side-buttons{--side-buttons-width: 120px;--side-buttons-margin: calc(var(--side-buttons-width) / 3);z-index:13;position:absolute;top:0;bottom:0;width:var(--side-buttons-width)}.zoom-modal .side-buttons ion-icon{top:50%;color:#fff;font-size:2.5rem;position:absolute;opacity:0;transition-duration:.15s;transition-timing-function:ease-in}.zoom-modal .side-buttons:hover{cursor:pointer}.zoom-modal .side-buttons:hover ion-icon{opacity:1}.zoom-modal .side-buttons.prev{left:0}.zoom-modal .side-buttons.prev ion-icon{left:var(--side-buttons-margin)}.zoom-modal .side-buttons.next{right:0}.zoom-modal .side-buttons.next ion-icon{right:var(--side-buttons-margin)}.zoom-modal swiper-container{height:100%;background:#000}.zoom-modal ion-fab-button .icon-secondary{top:11px;left:6px;height:14px}\n"] }]
29436
+ args: [{ selector: 'app-image-gallery', animations: [fadeInAnimation, fadeInOutAnimation], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-toolbar color=\"light\" class=\"gallery-toolbar\" *ngIf=\"showToolbar && !_inlineSlideshowActive\">\n <ng-content select=\"ion-buttons[slot=start]\"></ng-content>\n\n <!-- Add -->\n @if (!readOnly && showAddToolbarButton && enabled) {\n <button\n mat-icon-button\n [title]=\"!showTooltip ? ('COMMON.BTN_ADD' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_ADD' | translate) : ''\"\n (click)=\"add()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n\n <ng-content select=\"ion-buttons[slot=end]\"></ng-content>\n\n <ion-buttons slot=\"end\">\n <!-- Toggle view mode -->\n <ion-segment (ionChange)=\"toggleViewMode($event)\" color=\"accent\" [value]=\"_colSize\">\n <ion-segment-button [value]=\"4\">\n <mat-icon>view_module</mat-icon>\n </ion-segment-button>\n <ion-segment-button [value]=\"12\">\n <mat-icon>view_list</mat-icon>\n </ion-segment-button>\n </ion-segment>\n </ion-buttons>\n</ion-toolbar>\n\n<div\n class=\"gallery-container ion-padding-bottom {{ mode }}\"\n [class.inline-zoom-active]=\"_inlineSlideshowActive\"\n [style.--inline-zoom-height]=\"inlineZoomHeight\"\n>\n <div *ngIf=\"mobile && zoomActive\" class=\"backdrop\" [style.opacity]=\"zoomScale\"></div>\n\n <ion-grid class=\"ion-no-padding\" [class.cdk-visually-hidden]=\"_inlineSlideshowActive\">\n <ion-row>\n <ion-col *rxFor=\"let row of _data$; index as index; trackBy: trackByFn\" [size]=\"_colSize\">\n @let image = row | propertyGet: 'currentData';\n @if (cardTemplate) {\n <ng-container\n *ngTemplateOutlet=\"cardTemplate; context: { $implicit: row, mode: mode, image: image }\"\n ></ng-container>\n } @else {\n <ion-card\n #card\n class=\"image-card\"\n [class.zoom-hover]=\"!mobile\"\n [color]=\"cardColor\"\n (click)=\"clickRow(row, index)\"\n tappable\n @fadeInAnimation\n >\n <figure class=\"card-thumbnail\">\n @if (image.url) {\n <ion-img [src]=\"image.url | appendQueryParams: getImageSizeQueryParams()\" />\n } @else {\n <ion-img [src]=\"image.dataUrl\" />\n }\n </figure>\n\n <!-- toolbar for title (when not hover) -->\n @if (showTitle && image.title) {\n <ion-toolbar color=\"transparent\" class=\"title\">\n <ion-label class=\"card-title\">{{ image.title }}</ion-label>\n </ion-toolbar>\n }\n\n <!-- action toolbar shown when hover image -->\n @if (!readOnly && !mobile && showCardToolbar) {\n <ion-toolbar #cardToolbar color=\"light\" class=\"buttons\">\n <!-- edit title button -->\n @if (showTitle) {\n <button\n mat-button\n slot=\"start\"\n [disabled]=\"disabled\"\n (click)=\"editTitle($event, row, cardToolbar)\"\n (focusin)=\"focusCardToolbar(cardToolbar)\"\n >\n <mat-icon>edit</mat-icon>\n <mat-label>\n <span *ngIf=\"row.currentData.title | isNotNilOrBlank; else editLabel\" translate>\n IMAGE.GALLERY.BTN_EDIT_TITLE\n </span>\n <ng-template #editLabel>\n <span translate>IMAGE.GALLERY.BTN_ADD_TITLE</span>\n </ng-template>\n </mat-label>\n </button>\n }\n\n <div class=\"flex-spacer\"></div>\n\n <!-- delete button -->\n <button\n mat-icon-button\n slot=\"end\"\n class=\"ion-float-end\"\n [title]=\"!showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [disabled]=\"disabled\"\n (click)=\"delete($event, row)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </ion-toolbar>\n }\n </ion-card>\n }\n </ion-col>\n\n <!-- add button -->\n @if ((showAddTextButton || showAddCardButton) && !readOnly) {\n <ion-col [size]=\"_colSize\" @fadeInAnimation>\n @if (showAddTextButton) {\n <ion-button (click)=\"add($event)\" [disabled]=\"disabled\" [color]=\"addButtonColor\">\n <mat-label translate>{{ addButtonText }}</mat-label>\n <ion-icon slot=\"end\" name=\"camera\"></ion-icon>\n </ion-button>\n } @else if (showAddCardButton) {\n <ion-card\n class=\"image-card-button\"\n color=\"light\"\n (click)=\"add($event)\"\n [disabled]=\"disabled\"\n [title]=\"!showTooltip ? addButtonText : ''\"\n [matTooltip]=\"showTooltip ? addButtonText : ''\"\n tappable\n >\n <ion-card-content>\n <div class=\"icon-container\">\n <ion-icon name=\"camera\" [color]=\"addButtonColor\"></ion-icon>\n <ion-icon name=\"add\" [color]=\"addButtonColor\" class=\"icon-secondary\"></ion-icon>\n </div>\n </ion-card-content>\n </ion-card>\n }\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n\n <!-- Inline slideshow: shown as an overlay directly within the gallery, instead of a full-screen modal.\n The rest of the page (outside the gallery) stays fully visible and usable, unlike the modal's backdrop. -->\n @if (zoomPresentation === 'inline' && _inlineSlideshowActive) {\n <app-image-gallery-slideshow\n class=\"inline-overlay\"\n @fadeInOutAnimation\n [rows]=\"rows\"\n [selectedIndex]=\"_selectedRowIndex\"\n [mobile]=\"mobile\"\n [readOnly]=\"readOnly\"\n [disabled]=\"disabled\"\n [debug]=\"debug\"\n [mouseZoomEnabled]=\"_mouseZoomEnabled\"\n [wheelZoomStep]=\"wheelZoomStep\"\n [maxZoomRatio]=\"maxZoomRatio\"\n [rotationEnabled]=\"_rotationEnabled\"\n [cropEnabled]=\"_cropEnabled\"\n [imageExportFormat]=\"imageExportFormat\"\n [imageExportQuality]=\"imageExportQuality\"\n [imageSizeQueryParam]=\"imageSizeQueryParam\"\n [modalImageSize]=\"imageSizes?.modal\"\n (deleteRequested)=\"onSlideshowDeleteRequested($event)\"\n (closeRequested)=\"closeInlineSlideshow()\"\n (afterEditImage)=\"onAfterEditImage.emit($event)\"\n (afterEditRow)=\"onAfterEditRow.emit($event)\"\n ></app-image-gallery-slideshow>\n }\n</div>\n\n<!-- FAB button: add -->\n<ion-fab\n slot=\"fixed\"\n vertical=\"bottom\"\n horizontal=\"end\"\n *ngIf=\"showFabButton && !readOnly\"\n [class.cdk-visually-hidden]=\"disabled\"\n @fadeInOutAnimation\n>\n <ion-fab-button color=\"tertiary\" (click)=\"add($event)\">\n <ion-icon name=\"camera\"></ion-icon>\n <ion-icon name=\"add\" class=\"icon-secondary\" style=\"left: 33px; top: 9px; font-size: 20px\"></ion-icon>\n </ion-fab-button>\n</ion-fab>\n\n<!-- Zoom modal -->\n@if (zoomPresentation === 'modal') {\n <ion-modal #zoomModal class=\"stack-modal zoom-modal\" [class.modal-fullscreen]=\"mobile\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <app-image-gallery-slideshow\n [rows]=\"rows\"\n [selectedIndex]=\"_selectedRowIndex\"\n [mobile]=\"mobile\"\n [readOnly]=\"readOnly\"\n [disabled]=\"disabled\"\n [debug]=\"debug\"\n [mouseZoomEnabled]=\"_mouseZoomEnabled\"\n [wheelZoomStep]=\"wheelZoomStep\"\n [maxZoomRatio]=\"maxZoomRatio\"\n [rotationEnabled]=\"_rotationEnabled\"\n [cropEnabled]=\"_cropEnabled\"\n [imageExportFormat]=\"imageExportFormat\"\n [imageExportQuality]=\"imageExportQuality\"\n [imageSizeQueryParam]=\"imageSizeQueryParam\"\n [modalImageSize]=\"imageSizes?.modal\"\n (deleteRequested)=\"onSlideshowDeleteRequested($event)\"\n (closeRequested)=\"zoomModal.dismiss()\"\n (afterEditImage)=\"onAfterEditImage.emit($event)\"\n (afterEditRow)=\"onAfterEditRow.emit($event)\"\n ></app-image-gallery-slideshow>\n </ng-template>\n </ion-modal>\n}\n\n<ion-popover #titlePopover>\n <ng-template>\n <ion-content class=\"ion-padding\" cdkTrapFocus [cdkTrapFocusAutoCapture]=\"true\">\n <form [formGroup]=\"_titleForm\">\n <mat-form-field floatLabel=\"auto\">\n <mat-label translate>IMAGE.GALLERY.TITLE</mat-label>\n <input\n matInput\n formControlName=\"title\"\n [readonly]=\"disabled\"\n [appAutofocus]=\"_titleAutofocus\"\n (keydown.control.enter)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n />\n </mat-form-field>\n </form>\n </ion-content>\n\n <ion-footer>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"titlePopover.dismiss(null, 'CANCEL')\">\n <ion-label translate>COMMON.BTN_CANCEL</ion-label>\n </ion-button>\n <ion-button\n [fill]=\"disabled ? 'clear' : 'solid'\"\n (click)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n (keyup.enter)=\"titlePopover.dismiss(_titleForm.controls.title.value)\"\n color=\"tertiary\"\n [disabled]=\"disabled\"\n [title]=\"!showTooltip ? ('COMMON.BTN_VALIDATE_WITH_SHORTCUT_HELP' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_VALIDATE_WITH_SHORTCUT_HELP' | translate) : ''\"\n >\n <ion-label translate>COMMON.BTN_VALIDATE</ion-label>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n </ion-footer>\n </ng-template>\n</ion-popover>\n", styles: [":host{--image-margin-inline: 10px;--image-margin-safe-area: var(--image-margin-inline, 0px) * 2 - 3px;--segment-max-width: 150px}ion-toolbar{position:sticky;top:0}ion-toolbar ion-segment{max-width:var(--segment-max-width)}mat-toolbar,.mat-toolbar-single-row{padding:0;height:42px}ion-card{margin-left:unset;margin-right:unset;margin-inline:var(--image-margin-inline, 10px)}.image-slide{overflow:visible}.image-card{background-color:var(--ion-color-base);z-index:9;--img-scale: 1}.image-card .card-thumbnail{position:relative;overflow:hidden;display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:100%;height:100%;max-height:var(--img-max-height, calc(100% - var(--image-margin-safe-area, 0px)))}.image-card .card-thumbnail>ion-img{min-height:100%;height:100%;object-fit:cover;object-position:center;transition:scale .2s ease-in-out;scale:var(--img-scale)}.image-card .card-thumbnail>ion-img img{max-height:var(--img-max-height)}.image-card.zoom-hover:hover{--img-scale: 1.2}.image-card ion-toolbar{--top-offset: calc(-1 * var(--ion-toolbar-height));position:absolute}.image-card ion-toolbar.title{--padding-start: 16px;--padding-end: 16px;--ion-color-base: rgba(0, 0, 0, .45) !important;position:absolute;top:calc(100% + var(--top-offset));transition:opacity .2s ease-in-out}.image-card ion-toolbar.title ion-label{color:#fff}.image-card ion-toolbar.buttons{--padding-start: 4px;--padding-end: 4px;--ion-color-base: var(--ion-color-light) !important;position:absolute;top:100%;transition:transform .2s ease-in-out}.image-card:hover ion-toolbar.buttons,.image-card ion-toolbar.buttons.focused{transform:translateY(var(--top-offset))}.image-card-button{height:calc(100% - var(--image-margin-safe-area, 0px));max-height:max(min(30vh,200px),var(--img-max-height, 0px));transition:.2s ease-in-out;max-width:250px}.image-card-button ion-card-content{height:100%}.image-card-button .icon-container{height:auto;position:relative;top:calc(50% - 75px);display:flex;align-items:center;justify-content:center}.image-card-button ion-icon:first-of-type{font-size:150px;height:150px;position:relative;top:0}.image-card-button ion-icon.icon-secondary{font-size:50px;position:absolute;top:0;left:max(min(50% + 25px,100% - 50px),0px)}.image-card-button ion-button{float:right}.image-card-button:hover{--ion-color-base: var(--ion-color-shade) !important}.image-card-button:hover ion-icon{--ion-color-base: var(--ion-color-shade) !important}.backdrop{height:100vh;width:100vw;background:#000;position:absolute;z-index:10}.gallery-container{position:relative}.gallery-container.inline-zoom-active{min-height:var(--inline-zoom-height, 60vh)}.inline-overlay{position:absolute;inset:0;z-index:20;background:#000}\n"] }]
28786
29437
  }], ctorParameters: () => [{ type: ImageService }, { type: i2$1.Platform }, { type: i2$1.AlertController }, { type: i1$1.TranslateService }, { type: i0.ChangeDetectorRef }, { type: Environment, decorators: [{
28787
29438
  type: Optional
28788
29439
  }, {
@@ -28837,6 +29488,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
28837
29488
  type: Input
28838
29489
  }], imageSizeQueryParam: [{
28839
29490
  type: Input
29491
+ }], enableMouseZoom: [{
29492
+ type: Input,
29493
+ args: [{ transform: booleanAttribute }]
29494
+ }], wheelZoomStep: [{
29495
+ type: Input
29496
+ }], maxZoomRatio: [{
29497
+ type: Input
29498
+ }], enableRotation: [{
29499
+ type: Input,
29500
+ args: [{ transform: booleanAttribute }]
29501
+ }], enableCrop: [{
29502
+ type: Input,
29503
+ args: [{ transform: booleanAttribute }]
29504
+ }], imageExportFormat: [{
29505
+ type: Input
29506
+ }], imageExportQuality: [{
29507
+ type: Input
29508
+ }], zoomPresentation: [{
29509
+ type: Input
29510
+ }], inlineZoomHeight: [{
29511
+ type: Input
28840
29512
  }], dataSource: [{
28841
29513
  type: Input
28842
29514
  }], zoomModal: [{
@@ -28845,15 +29517,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
28845
29517
  }], titlePopover: [{
28846
29518
  type: ViewChild,
28847
29519
  args: ['titlePopover']
28848
- }], swiperRef: [{
28849
- type: ViewChild,
28850
- args: ['modalSwiper']
28851
29520
  }], onBeforeDeleteRows: [{
28852
29521
  type: Output
28853
29522
  }], onAfterAddRows: [{
28854
29523
  type: Output
28855
29524
  }], onAfterEditRow: [{
28856
29525
  type: Output
29526
+ }], onAfterEditImage: [{
29527
+ type: Output
28857
29528
  }], click: [{
28858
29529
  type: Output
28859
29530
  }] } });
@@ -28862,7 +29533,8 @@ class ImageGalleryModule {
28862
29533
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageGalleryModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
28863
29534
  static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.14", ngImport: i0, type: ImageGalleryModule, declarations: [
28864
29535
  // Components
28865
- AppImageGalleryComponent], imports: [CommonModule,
29536
+ AppImageGalleryComponent,
29537
+ AppImageGallerySlideshowComponent], imports: [CommonModule,
28866
29538
  IonicModule,
28867
29539
  ReactiveFormsModule,
28868
29540
  SharedMaterialModule,
@@ -28870,7 +29542,8 @@ class ImageGalleryModule {
28870
29542
  SharedDirectivesModule, i1$1.TranslateModule, RxStateModule], exports: [IonicModule,
28871
29543
  TranslateModule,
28872
29544
  // Components
28873
- AppImageGalleryComponent] });
29545
+ AppImageGalleryComponent,
29546
+ AppImageGallerySlideshowComponent] });
28874
29547
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageGalleryModule, imports: [CommonModule,
28875
29548
  IonicModule,
28876
29549
  ReactiveFormsModule,
@@ -28898,12 +29571,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
28898
29571
  declarations: [
28899
29572
  // Components
28900
29573
  AppImageGalleryComponent,
29574
+ AppImageGallerySlideshowComponent,
28901
29575
  ],
28902
29576
  exports: [
28903
29577
  IonicModule,
28904
29578
  TranslateModule,
28905
29579
  // Components
28906
29580
  AppImageGalleryComponent,
29581
+ AppImageGallerySlideshowComponent,
28907
29582
  ],
28908
29583
  }]
28909
29584
  }] });
@@ -51169,16 +51844,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
51169
51844
 
51170
51845
  class GalleryTestPage {
51171
51846
  dataService;
51847
+ route;
51172
51848
  _subscription = new Subscription();
51173
51849
  dataSource;
51174
- constructor(dataService) {
51850
+ zoomPresentation = 'modal';
51851
+ debug = false;
51852
+ constructor(dataService, route) {
51175
51853
  this.dataService = dataService;
51854
+ this.route = route;
51176
51855
  this.dataSource = new EntitiesTableDataSource(ImageAttachment, this.dataService, null, {
51177
51856
  saveBeforeDelete: true,
51178
51857
  saveOnlyDirtyRows: false,
51179
51858
  });
51859
+ this.debug = this.route.snapshot.queryParamMap.get('debug') === 'true';
51180
51860
  }
51181
51861
  ngOnInit() {
51862
+ this.zoomPresentation = this.route.snapshot.queryParamMap.get('zoomPresentation') || 'modal';
51182
51863
  this._subscription.add(this.dataSource.watchAll(0, 100, null, null, null).subscribe());
51183
51864
  }
51184
51865
  ngOnDestroy() {
@@ -51187,13 +51868,13 @@ class GalleryTestPage {
51187
51868
  save() {
51188
51869
  this.dataSource.save();
51189
51870
  }
51190
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GalleryTestPage, deps: [{ token: ImageAttachmentService }], target: i0.ɵɵFactoryTarget.Component });
51191
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: GalleryTestPage, selector: "app-gallery-test-page", providers: [{ provide: ImageAttachmentService, useClass: ImageAttachmentService }], ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Image gallery</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <!-- DEBUG -->\n <app-debug title=\"Galley\">\n <ion-grid>\n <ion-row>\n <ion-col>gallery.count: {{ gallery.rows?.length }}</ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n\n <app-image-gallery\n #gallery\n [dataSource]=\"dataSource\"\n (onAfterAddRows)=\"save()\"\n (onAfterEditRow)=\"save()\"\n ></app-image-gallery>\n</ion-content>\n", dependencies: [{ kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: AppImageGalleryComponent, selector: "app-image-gallery", inputs: ["cardColor", "debug", "disabled", "readOnly", "mobile", "mode", "confirmBeforeDelete", "showToolbar", "showFabButton", "showTitle", "showAddToolbarButton", "showAddTextButton", "showAddCardButton", "showCardToolbar", "addButtonColor", "addButtonText", "cardTemplate", "imageSizes", "imageSizeQueryParam", "dataSource"], outputs: ["onBeforeDeleteRows", "onAfterAddRows", "onAfterEditRow", "click"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }] });
51871
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GalleryTestPage, deps: [{ token: ImageAttachmentService }, { token: i1$7.ActivatedRoute }], target: i0.ɵɵFactoryTarget.Component });
51872
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: GalleryTestPage, selector: "app-gallery-test-page", providers: [{ provide: ImageAttachmentService, useClass: ImageAttachmentService }], ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Image gallery</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <!-- DEBUG -->\n @if (debug) {\n <app-debug title=\"Galley\">\n <ion-grid>\n <ion-row>\n <ion-col>gallery.count: {{ gallery.rows?.length }}</ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n }\n\n <app-image-gallery\n #gallery\n [dataSource]=\"dataSource\"\n [zoomPresentation]=\"zoomPresentation\"\n [inlineZoomHeight]=\"'100%'\"\n (onAfterAddRows)=\"save()\"\n (onAfterEditRow)=\"save()\"\n [debug]=\"debug\"\n ></app-image-gallery>\n</ion-content>\n", dependencies: [{ kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: AppImageGalleryComponent, selector: "app-image-gallery", inputs: ["cardColor", "debug", "disabled", "readOnly", "mobile", "mode", "confirmBeforeDelete", "showToolbar", "showFabButton", "showTitle", "showAddToolbarButton", "showAddTextButton", "showAddCardButton", "showCardToolbar", "addButtonColor", "addButtonText", "cardTemplate", "imageSizes", "imageSizeQueryParam", "enableMouseZoom", "wheelZoomStep", "maxZoomRatio", "enableRotation", "enableCrop", "imageExportFormat", "imageExportQuality", "zoomPresentation", "inlineZoomHeight", "dataSource"], outputs: ["onBeforeDeleteRows", "onAfterAddRows", "onAfterEditRow", "onAfterEditImage", "click"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }] });
51192
51873
  }
51193
51874
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GalleryTestPage, decorators: [{
51194
51875
  type: Component,
51195
- args: [{ selector: 'app-gallery-test-page', providers: [{ provide: ImageAttachmentService, useClass: ImageAttachmentService }], template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Image gallery</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <!-- DEBUG -->\n <app-debug title=\"Galley\">\n <ion-grid>\n <ion-row>\n <ion-col>gallery.count: {{ gallery.rows?.length }}</ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n\n <app-image-gallery\n #gallery\n [dataSource]=\"dataSource\"\n (onAfterAddRows)=\"save()\"\n (onAfterEditRow)=\"save()\"\n ></app-image-gallery>\n</ion-content>\n" }]
51196
- }], ctorParameters: () => [{ type: ImageAttachmentService }] });
51876
+ args: [{ selector: 'app-gallery-test-page', providers: [{ provide: ImageAttachmentService, useClass: ImageAttachmentService }], template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Image gallery</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <!-- DEBUG -->\n @if (debug) {\n <app-debug title=\"Galley\">\n <ion-grid>\n <ion-row>\n <ion-col>gallery.count: {{ gallery.rows?.length }}</ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n }\n\n <app-image-gallery\n #gallery\n [dataSource]=\"dataSource\"\n [zoomPresentation]=\"zoomPresentation\"\n [inlineZoomHeight]=\"'100%'\"\n (onAfterAddRows)=\"save()\"\n (onAfterEditRow)=\"save()\"\n [debug]=\"debug\"\n ></app-image-gallery>\n</ion-content>\n" }]
51877
+ }], ctorParameters: () => [{ type: ImageAttachmentService }, { type: i1$7.ActivatedRoute }] });
51197
51878
 
51198
51879
  const routes$8 = [
51199
51880
  {
@@ -53931,5 +54612,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
53931
54612
  * Generated bundle index. Do not edit.
53932
54613
  */
53933
54614
 
53934
- export { APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_ACCOUNT_SERVICE, APP_ACCOUNT_SERVICE_OPTIONS, APP_CELL_SELECTION_SERVICE_CONFIG_TOKEN, APP_CELL_SELECTION_SERVICE_TOKEN, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOME_TOOLBAR_BUTTONS, APP_HOTKEYS_CONFIG, APP_JOB_PROGRESSION_SERVICE, APP_LOCALES, APP_LOCAL_SETTINGS, APP_LOCAL_SETTINGS_OPTIONS, APP_LOCAL_STORAGE_TYPE_POLICIES, APP_LOGGING_SERVICE, APP_MENU_ITEMS, APP_MENU_OPTIONS, APP_NAMED_FILTER_SERVICE, APP_PERSON_SERVICE, APP_PERSON_SERVICE_OPTIONS, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_SHOW_TOOLTIP, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, AbstractPersonService, AbstractSelectionModelPipe, AbstractTableSelectionPipe, AbstractUserEventService, Account, AccountPage, AccountService, AccountToStringPipe, AccountUtils, ActionsColumnComponent, AdminModule, AdminRoutingModule, AdminUsersModule, Alerts, AndroidOsEnvironment, AppAboutModalModule, AppAccountModule, AppAsyncTable, AppAuthForm, AppAuthModal, AppAuthModule, AppChangePasswordModule, AppChangePasswordPage, AppEditor, AppEditorOptions, AppEntityEditor, AppEntityEditorModal, AppEntityEditorModalOptions, AppEntityFormModule, AppForm, AppFormArray, AppFormButtonsBarModule, AppFormContainer, AppFormField, AppFormModule, AppFormProvider, AppFormUtils, AppGestureConfig, AppGraphQLModule, AppHomePageModule, AppIconComponent, AppIconModule, AppImageGalleryComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSelectUsersModal, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextFormModule, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayMapPipe, ArrayPluckPipe, ArraySlicePipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutoTooltipDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_TESTING_PAGES, CapitalizePipe, CellIdentifierDirective, CellSelectionDirective, CellSelectionService, CellValueChangeListener, ChangeCaseToUnderscorePipe, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, DATE_ISO_PATTERNS, DATE_MATCH_REGEXP, DATE_PATTERN, DATE_UNIX_MS_TIMESTAMP, DATE_UNIX_TIMESTAMP, DEFAULT_JOIN_ARRAY_VALUES_SEPARATOR, DEFAULT_JOIN_PROPERTIES_SEPARATOR, DEFAULT_MENU_SHOW_WHEN, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLACEHOLDER_CHAR, DEFAULT_REQUIRED_COLUMNS, DateDiffDurationPipe, DateFormatPipe, DateFormatService, DateFromNowPipe, DateFromPipe, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FeedDirective, FeedModule, FeedPage, FeedService, FeedsComponent, FileResponse, FileService, FileSizePipe, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayAtControlPipe, FormArrayAtGroupPipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetNamePipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, IMAGE_DEFAULTS, IPosition, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImagesUtils, InMemoryEntitiesService, IsAllSelectedPipe, IsEmptySelectionPipe, IsLoginAccountPipe, IsMultipleSelectionPipe, IsNilOrBlankPipe, IsNilOrNaNPipe, IsNilPipe, IsNotAllSelectedPipe, IsNotEmptySelectionPipe, IsNotNilOrBlankPipe, IsNotNilOrNaNPipe, IsNotNilPipe, IsOnDeskPipe, IsOnFieldPipe, IsSelectedPipe, IsSingleSelectionPipe, IsValidDatePipe, JobModule, JobProgression, JobProgressionComponent, JobProgressionIcon, JobProgressionList, JobProgressionService, JobProgressionTestService, JobProgressionTestingPage, JobTestingModule, JobUtils, JsonFeedUtils, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, LocalSettingsService, LogLevel, LogUtils, Logger, LoggingService, LoggingServiceModule, LongitudeFormatPipe, MASKS, MASK_RANGES, MAT_FORM_FIELD_DEFAULT_APPEARANCE, MAT_FORM_FIELD_DEFAULT_SUBSCRIPT_SIZING, MINIFY_ENTITY_FOR_LOCAL_STORAGE, MINIFY_ENTITY_FOR_POD, MOMENT_NO_TIME_PROPERTY, MapGetPipe, MapKeysPipe, MapPipe, MapToPipe, MapValuesPipe, MarkdownDirective, MarkdownService, MarkdownTestPage, MarkdownTestingModule, MarkdownUtils, MaskitoPlaceholderPipe, MaskitoTestPage, MatAutocompleteConfigHolder, MatAutocompleteField, MatAutocompleteFieldUtils, MatBadgeTestPage, MatBooleanField, MatChipsField, MatColorPipe, MatCommonTestPage, MatDate, MatDateShort, MatDateTime, MatDuration, MatLatLongField, MatLatLongFieldInput, MatPaginatorI18n, MatStepperI18n, MatSwipeField, MaterialAutocompleteFooterDirective, MaterialAutocompleteHeaderDirective, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NavActionsColumnComponent, NestedTableTestPage, NetworkService, NetworkUtils, NewTokenForm, NewTokenModal, NgInitDirective, NgVarDirective, NoHtmlPipe, NotEmptyArrayPipe, NumberFormatPipe, ObservableTestPage, OddPipe, OtherMenuTestingPage, PEER_URL_REGEXP, PLUS_PLACEHOLDER_CHAR_REGEXP_GLOBAL, PRINT_ID_QUERY_PARAM, PRINT_LOADING_STORAGE_KEY_PREFIX, PRIORITIZED_AUTHORITIES, PUBKEY_REGEXP, Peer, Person, PersonFilter, PersonFilterAdditionalFields, PersonFragments, PersonService, PersonToStringPipe, PersonUtils, PersonValidatorService, PlatformService, PrintService, ProgressBarService, ProgressInterceptor, PropertiesFormTestPage, PropertiesFormTestingModule, PropertyEntity, PropertyEntityFilter, PropertyEntityValidator, PropertyFormatPipe, PropertyGetPipe, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS, Referential, ReferentialFilter, ReferentialRef, ReferentialToStringPipe, ReferentialUtils, ReferentialValidatorService, ReferentialsToStringPipe, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, RxStateOutput, RxStateProperty, RxStateRegister, RxStateSelect, SCRYPT_PARAMS, SETTINGS_COMPACT_ROWS, SETTINGS_DISPLAY_COLUMNS, SETTINGS_FILTER, SETTINGS_PAGE_SIZE, SETTINGS_SORTED_COLUMN, SETTINGS_STORAGE_KEY, SETTINGS_TRANSIENT_PROPERTIES, SHARED_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, STARTUP_DATA_STORAGE_KEY, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StartupService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingModule, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, TruncateHtmlPipe, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, UsersUtils, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, assignSkipUndefined, base64ArrayBuffer, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, collectByPropertyPath, compareValues, compareValuesDesc, compareVersionNumbers, composeComparators, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createAppStartupInitializer, createPromiseEvent, createPromiseEventEmitter, decorateWithTakeUntil, deepMergeSkipUndefined, departmentToString, departmentsToString, disableAndClearControl, disableAndClearControls, disableControl, disableControls, emitPromiseEvent, enableControl, enableControls, enableRxStateProdMode, entityToString, equals, equalsOrNil, escapeRegExp, expansionAnimation, fadeInAnimation, fadeInOutAnimation, fadeInSlowAnimation, filterFalse, filterFormErrors, filterFormErrorsByPath, filterFormErrorsByPrefix, filterNotNil, filterNumberInput, filterTrue, findParentWithClass, firstArrayValue, firstFalse, firstFalsePromise, firstNotNil, firstNotNilPromise, firstPromise, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorLuminance, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hexToRgb, hexToRgbArray, initArrayControlsFromValues, interpolateString, intersectArrays, isAndroid, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isFocusableElement, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isLightColor, isMacOS, isMobile, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isTouchUi, isVersionCompatible, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mergeObjectsWithoutUndefined, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseLatitudeOrLongitude, propertiesPathComparator, propertyComparator, propertyPathComparator, provideAccountService, providePersonService, referentialToString, referentialsToString, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, setControlRequired, setControlsEnabled, setFormErrors, setPropertyByPath, setTabIndex, sleep, slideDownAnimation, slideInAnimation, slideInOutAnimation, slideUpDownAnimation, sort, splitArrayInChunks, splitById, splitByProperty, splitDegreesToDDArray, splitDegreesToDDMMArray, splitDegreesToDDMMSSArray, startsWithUpperCase, suggestFromArray, suggestFromStringArray, tabindexComparator, testUserAgent, toBoolean, toDateISOString, toDuration, toFloat, toInt, toLoadData, toLoadResult, toNotNil, toNumber, trimEmptyToNull, truncateHtml, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
54615
+ export { APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_ACCOUNT_SERVICE, APP_ACCOUNT_SERVICE_OPTIONS, APP_CELL_SELECTION_SERVICE_CONFIG_TOKEN, APP_CELL_SELECTION_SERVICE_TOKEN, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOME_TOOLBAR_BUTTONS, APP_HOTKEYS_CONFIG, APP_JOB_PROGRESSION_SERVICE, APP_LOCALES, APP_LOCAL_SETTINGS, APP_LOCAL_SETTINGS_OPTIONS, APP_LOCAL_STORAGE_TYPE_POLICIES, APP_LOGGING_SERVICE, APP_MENU_ITEMS, APP_MENU_OPTIONS, APP_NAMED_FILTER_SERVICE, APP_PERSON_SERVICE, APP_PERSON_SERVICE_OPTIONS, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_SHOW_TOOLTIP, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, AbstractPersonService, AbstractSelectionModelPipe, AbstractTableSelectionPipe, AbstractUserEventService, Account, AccountPage, AccountService, AccountToStringPipe, AccountUtils, ActionsColumnComponent, AdminModule, AdminRoutingModule, AdminUsersModule, Alerts, AndroidOsEnvironment, AppAboutModalModule, AppAccountModule, AppAsyncTable, AppAuthForm, AppAuthModal, AppAuthModule, AppChangePasswordModule, AppChangePasswordPage, AppEditor, AppEditorOptions, AppEntityEditor, AppEntityEditorModal, AppEntityEditorModalOptions, AppEntityFormModule, AppForm, AppFormArray, AppFormButtonsBarModule, AppFormContainer, AppFormField, AppFormModule, AppFormProvider, AppFormUtils, AppGestureConfig, AppGraphQLModule, AppHomePageModule, AppIconComponent, AppIconModule, AppImageGalleryComponent, AppImageGallerySlideshowComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSelectUsersModal, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextFormModule, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayMapPipe, ArrayPluckPipe, ArraySlicePipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutoTooltipDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_TESTING_PAGES, CapitalizePipe, CellIdentifierDirective, CellSelectionDirective, CellSelectionService, CellValueChangeListener, ChangeCaseToUnderscorePipe, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, DATE_ISO_PATTERNS, DATE_MATCH_REGEXP, DATE_PATTERN, DATE_UNIX_MS_TIMESTAMP, DATE_UNIX_TIMESTAMP, DEFAULT_JOIN_ARRAY_VALUES_SEPARATOR, DEFAULT_JOIN_PROPERTIES_SEPARATOR, DEFAULT_MENU_SHOW_WHEN, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLACEHOLDER_CHAR, DEFAULT_REQUIRED_COLUMNS, DateDiffDurationPipe, DateFormatPipe, DateFormatService, DateFromNowPipe, DateFromPipe, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FeedDirective, FeedModule, FeedPage, FeedService, FeedsComponent, FileResponse, FileService, FileSizePipe, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayAtControlPipe, FormArrayAtGroupPipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetNamePipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, IMAGE_DEFAULTS, IPosition, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImagesUtils, InMemoryEntitiesService, IsAllSelectedPipe, IsEmptySelectionPipe, IsLoginAccountPipe, IsMultipleSelectionPipe, IsNilOrBlankPipe, IsNilOrNaNPipe, IsNilPipe, IsNotAllSelectedPipe, IsNotEmptySelectionPipe, IsNotNilOrBlankPipe, IsNotNilOrNaNPipe, IsNotNilPipe, IsOnDeskPipe, IsOnFieldPipe, IsSelectedPipe, IsSingleSelectionPipe, IsValidDatePipe, JobModule, JobProgression, JobProgressionComponent, JobProgressionIcon, JobProgressionList, JobProgressionService, JobProgressionTestService, JobProgressionTestingPage, JobTestingModule, JobUtils, JsonFeedUtils, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, LocalSettingsService, LogLevel, LogUtils, Logger, LoggingService, LoggingServiceModule, LongitudeFormatPipe, MASKS, MASK_RANGES, MAT_FORM_FIELD_DEFAULT_APPEARANCE, MAT_FORM_FIELD_DEFAULT_SUBSCRIPT_SIZING, MINIFY_ENTITY_FOR_LOCAL_STORAGE, MINIFY_ENTITY_FOR_POD, MOMENT_NO_TIME_PROPERTY, MapGetPipe, MapKeysPipe, MapPipe, MapToPipe, MapValuesPipe, MarkdownDirective, MarkdownService, MarkdownTestPage, MarkdownTestingModule, MarkdownUtils, MaskitoPlaceholderPipe, MaskitoTestPage, MatAutocompleteConfigHolder, MatAutocompleteField, MatAutocompleteFieldUtils, MatBadgeTestPage, MatBooleanField, MatChipsField, MatColorPipe, MatCommonTestPage, MatDate, MatDateShort, MatDateTime, MatDuration, MatLatLongField, MatLatLongFieldInput, MatPaginatorI18n, MatStepperI18n, MatSwipeField, MaterialAutocompleteFooterDirective, MaterialAutocompleteHeaderDirective, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NavActionsColumnComponent, NestedTableTestPage, NetworkService, NetworkUtils, NewTokenForm, NewTokenModal, NgInitDirective, NgVarDirective, NoHtmlPipe, NotEmptyArrayPipe, NumberFormatPipe, ObservableTestPage, OddPipe, OtherMenuTestingPage, PEER_URL_REGEXP, PLUS_PLACEHOLDER_CHAR_REGEXP_GLOBAL, PRINT_ID_QUERY_PARAM, PRINT_LOADING_STORAGE_KEY_PREFIX, PRIORITIZED_AUTHORITIES, PUBKEY_REGEXP, Peer, Person, PersonFilter, PersonFilterAdditionalFields, PersonFragments, PersonService, PersonToStringPipe, PersonUtils, PersonValidatorService, PlatformService, PrintService, ProgressBarService, ProgressInterceptor, PropertiesFormTestPage, PropertiesFormTestingModule, PropertyEntity, PropertyEntityFilter, PropertyEntityValidator, PropertyFormatPipe, PropertyGetPipe, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS, Referential, ReferentialFilter, ReferentialRef, ReferentialToStringPipe, ReferentialUtils, ReferentialValidatorService, ReferentialsToStringPipe, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, RxStateOutput, RxStateProperty, RxStateRegister, RxStateSelect, SCRYPT_PARAMS, SETTINGS_COMPACT_ROWS, SETTINGS_DISPLAY_COLUMNS, SETTINGS_FILTER, SETTINGS_PAGE_SIZE, SETTINGS_SORTED_COLUMN, SETTINGS_STORAGE_KEY, SETTINGS_TRANSIENT_PROPERTIES, SHARED_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, STARTUP_DATA_STORAGE_KEY, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StartupService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingModule, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, TruncateHtmlPipe, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, UsersUtils, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, assignSkipUndefined, base64ArrayBuffer, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, collectByPropertyPath, compareValues, compareValuesDesc, compareVersionNumbers, composeComparators, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createAppStartupInitializer, createPromiseEvent, createPromiseEventEmitter, decorateWithTakeUntil, deepMergeSkipUndefined, departmentToString, departmentsToString, disableAndClearControl, disableAndClearControls, disableControl, disableControls, emitPromiseEvent, enableControl, enableControls, enableRxStateProdMode, entityToString, equals, equalsOrNil, escapeRegExp, expansionAnimation, fadeInAnimation, fadeInOutAnimation, fadeInSlowAnimation, filterFalse, filterFormErrors, filterFormErrorsByPath, filterFormErrorsByPrefix, filterNotNil, filterNumberInput, filterTrue, findParentWithClass, firstArrayValue, firstFalse, firstFalsePromise, firstNotNil, firstNotNilPromise, firstPromise, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorLuminance, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hexToRgb, hexToRgbArray, initArrayControlsFromValues, interpolateString, intersectArrays, isAndroid, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isFocusableElement, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isLightColor, isMacOS, isMobile, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isTouchUi, isVersionCompatible, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mergeObjectsWithoutUndefined, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseLatitudeOrLongitude, propertiesPathComparator, propertyComparator, propertyPathComparator, provideAccountService, providePersonService, referentialToString, referentialsToString, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, setControlRequired, setControlsEnabled, setFormErrors, setPropertyByPath, setTabIndex, sleep, slideDownAnimation, slideInAnimation, slideInOutAnimation, slideUpDownAnimation, sort, splitArrayInChunks, splitById, splitByProperty, splitDegreesToDDArray, splitDegreesToDDMMArray, splitDegreesToDDMMSSArray, startsWithUpperCase, suggestFromArray, suggestFromStringArray, tabindexComparator, testUserAgent, toBoolean, toDateISOString, toDuration, toFloat, toInt, toLoadData, toLoadResult, toNotNil, toNumber, trimEmptyToNull, truncateHtml, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
53935
54616
  //# sourceMappingURL=sumaris-net.ngx-components.mjs.map