tailjng 0.0.37 → 0.0.38

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.
@@ -50,10 +50,14 @@ function getComponentList() {
50
50
  path: "src/lib/components/progress-bar",
51
51
  dependencies: [],
52
52
  },
53
- 'image-viewer': {
54
- path: "src/lib/components/image/image-viewer",
53
+ 'viewer-image': {
54
+ path: "src/lib/components/viewer/viewer-image",
55
55
  dependencies: ["button"],
56
56
  },
57
+ 'viewer-pdf': {
58
+ path: "src/lib/components/viewer/viewer-pdf",
59
+ dependencies: [],
60
+ },
57
61
  'dialog': {
58
62
  path: "src/lib/components/dialog",
59
63
  dependencies: [],
@@ -114,10 +118,10 @@ function getComponentList() {
114
118
  path: "src/lib/components/menu/menu-options-table",
115
119
  dependencies: ["button"],
116
120
  },
117
-
118
-
119
-
120
-
121
+
122
+
123
+
124
+
121
125
  'table-crud-complete': {
122
126
  path: "src/lib/components/table/table-crud-complete",
123
127
  dependencies: ["button", "paginator-complete", "filter-complete", "checkbox-input", "menu-options-table", "dialog", "image-viewer", "select-dropdown", "input"],
@@ -27,7 +27,7 @@ Authors:
27
27
  License:
28
28
  This project is licensed under the BSD 3-Clause - see the LICENSE file for more details.
29
29
 
30
- Version: 0.0.37
30
+ Version: 0.0.38
31
31
  Creation Date: 2025-01-04
32
32
  ===============================================`
33
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailjng",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^19.2.0",
6
6
  "@angular/core": "^19.2.0",
@@ -3,18 +3,18 @@
3
3
  @if (isSearch) {
4
4
  <JFilter
5
5
  [params]="params"
6
- [columns]="columns"
7
- [endpoint]="endpoint"
8
- [mainEndpoint]="mainEndpoint"
9
- [data]="data"
10
- [(searchQuery)]="searchQuery"
11
- (search)="onSearch()"
6
+ [columns]="columns"
7
+ [endpoint]="endpoint"
8
+ [mainEndpoint]="mainEndpoint"
9
+ [data]="data"
10
+ [(searchQuery)]="searchQuery"
11
+ (search)="onSearch()"
12
12
  [searchPlaceholder]="searchPlaceholder"
13
13
  [(itemsPerPage)]="itemsPerPage"
14
- [itemsPerPageOptions]="itemsPerPageOptions"
14
+ [itemsPerPageOptions]="itemsPerPageOptions"
15
15
  (onItemsPerPageChangeEvent)="onItemsPerPageChange()"
16
- [isLoadingSearch]="isLoading('search')"
17
- [isLoadingPerPage]="isLoading('itemsPerPage')"
16
+ [isLoadingSearch]="isLoading('search')"
17
+ [isLoadingPerPage]="isLoading('itemsPerPage')"
18
18
  [isLoadingAditionalButtons]="loadingStates.aditionalButtons"
19
19
  (clearFilters)="onClearFilters($event)"
20
20
  [filtersButton]="filtersButton"
@@ -33,12 +33,12 @@
33
33
  @if (isPaginator) {
34
34
  <div class="my-4">
35
35
  <JCompletePaginator
36
- [currentPage]="currentPage"
37
- [itemsPerPageOptions]="itemsPerPageOptions"
36
+ [currentPage]="currentPage"
37
+ [itemsPerPageOptions]="itemsPerPageOptions"
38
38
  [itemsPerPage]="itemsPerPage"
39
- [totalItems]="totalItems"
40
- [pages]="pages"
41
- (pageChange)="handlePageChange($event)"
39
+ [totalItems]="totalItems"
40
+ [pages]="pages"
41
+ (pageChange)="handlePageChange($event)"
42
42
  [isLoading]="isLoading('pagination')"
43
43
  />
44
44
  </div>
@@ -57,25 +57,25 @@
57
57
  <div class="j-crud-card grid grid-cols-4 items-start max-[1300px]:grid-cols-3 max-[1150px]:grid-cols-2 max-[800px]:grid-cols-1 gap-4 my-4">
58
58
  @for (item of displayData; track item?.id) {
59
59
  <div class="flex flex-col gap-3 rounded-xl border border-border dark:border-dark-border bg-white dark:bg-foreground shadow-sm hover:shadow-lg hover:border-primary/50 hover:dark:border-primary transition-all duration-300">
60
- <ng-container
61
- *ngTemplateOutlet="itemTemplate; context: {
62
- $implicit: item,
63
- getValue: getValue.bind(this),
64
- columns: columns,
65
- item: item,
66
- template: getTemplate,
67
- toggleExpanded: toggleExpanded.bind(this),
68
- isExpanded: isExpanded.bind(this),
60
+ <ng-container
61
+ *ngTemplateOutlet="itemTemplate; context: {
62
+ $implicit: item,
63
+ getValue: getValue.bind(this),
64
+ columns: columns,
65
+ item: item,
66
+ template: getTemplate,
67
+ toggleExpanded: toggleExpanded.bind(this),
68
+ isExpanded: isExpanded.bind(this),
69
69
  expandData: getExpandData.bind(this),
70
70
  expandTemplate: expandTemplate,
71
71
  isLoadingExpand: isLoadingExpand.bind(this),
72
- }"
72
+ }"
73
73
  />
74
74
  </div>
75
75
  }
76
76
  </div>
77
-
78
-
77
+
78
+
79
79
  } @else {
80
80
  <div class="j-crud-card w-full flex justify-center items-center h-100 bg-white dark:bg-foreground rounded-[20px]">
81
81
  <p>No se ha definido ninguna plantilla personalizada (<code>itemTemplate</code>).</p>
@@ -83,20 +83,21 @@
83
83
  }
84
84
 
85
85
  } @else if (!isLoading('pagination')) {
86
- <div class="j-crud-card w-full flex justify-center items-center h-100 bg-white dark:bg-foreground rounded-[20px] border border-border dark:border-dark-border hover:border-primary/50 hover:dark:border-primary">
87
- No hay datos disponibles
86
+ <div class="j-crud-card w-full flex flex-col gap-3 justify-center items-center h-100 bg-white dark:bg-foreground rounded-[20px] border border-border dark:border-dark-border hover:border-primary/50 hover:dark:border-primary">
87
+ <lucide-icon [name]="iconsService.icons.info" size="35" class="text-primary"></lucide-icon>
88
+ <p class="text-black dark:text-white">No hay datos disponibles</p>
88
89
  </div>
89
90
  }
90
91
 
91
92
  @if (isPaginator) {
92
93
  <div class="my-4">
93
94
  <JCompletePaginator
94
- [currentPage]="currentPage"
95
- [itemsPerPageOptions]="itemsPerPageOptions"
95
+ [currentPage]="currentPage"
96
+ [itemsPerPageOptions]="itemsPerPageOptions"
96
97
  [itemsPerPage]="itemsPerPage"
97
- [totalItems]="totalItems"
98
- [pages]="pages"
99
- (pageChange)="handlePageChange($event)"
98
+ [totalItems]="totalItems"
99
+ [pages]="pages"
100
+ (pageChange)="handlePageChange($event)"
100
101
  [isLoading]="isLoading('pagination')"
101
102
  />
102
103
  </div>
@@ -4,39 +4,47 @@
4
4
  @if (overlay) {
5
5
  <div class="fixed inset-0 z-[999] bg-black/50"></div>
6
6
  }
7
-
7
+
8
8
  <!-- Modal -->
9
9
  <div class="fixed inset-0 z-[1000] flex pointer-events-none" [ngClass]="getPositionClass()">
10
-
11
- <div @modalTransition
12
- class="pointer-events-auto bg-white dark:bg-foreground rounded-[12px] shadow-lg border-2 border-border dark:border-dark-border"
13
- [ngStyle]="getOffsetStyles()"
14
- data-draggable-dialog
15
- >
16
-
10
+
11
+ <div @modalTransition
12
+ class="pointer-events-auto bg-white dark:bg-foreground rounded-[12px] shadow-lg border-2 border-border dark:border-dark-border"
13
+ [ngStyle]="getOffsetStyles()"
14
+ [style.width]="getModalWidth()"
15
+ [style.min-width]="!isFullScreen() ? '200px' : null"
16
+ [style.min-height]="!isFullScreen() ? '40px' : null"
17
+ data-draggable-dialog
18
+ >
17
19
  <!-- Header draggable -->
18
20
  @if (draggable) {
19
- <div class="flex p-1 pl-4 pr-4 justify-between items-center bg-primary dark:bg-dark-primary border-b border-border dark:border-dark-border rounded-[10px] font-semibold text-2sm cursor-move select-none"
20
- (mousedown)="$event.stopPropagation(); startDrag($event)">
21
- <h3 class="text-[1em] font-semibold text-white leading-none">{{ title }}</h3>
21
+ <div
22
+ class="flex items-center p-1 pl-4 pr-4 gap-2 bg-primary dark:bg-dark-primary border-b border-border dark:border-dark-border rounded-[10px] font-semibold text-2sm cursor-move select-none"
23
+ (mousedown)="$event.stopPropagation(); startDrag($event)"
24
+ >
25
+ <h3
26
+ class="flex-1 min-w-0 text-[1em] font-semibold text-white leading-none truncate"
27
+ >
28
+ {{ title }}
29
+ </h3>
22
30
 
23
- <button
24
- type="button"
31
+ <button
32
+ type="button"
25
33
  (click)="$event.stopPropagation(); onClose()"
26
- class="p-2 rounded-full border border-border dark:border-dark-border text-white hover:bg-dark-background focus:outline-none cursor-pointer"
34
+ class="flex-shrink-0 p-2 rounded-full border border-border dark:border-dark-border text-white hover:bg-dark-background focus:outline-none cursor-pointer"
27
35
  >
28
36
  <lucide-icon [name]="iconsService.icons.close" size="16"></lucide-icon>
29
37
  </button>
30
-
31
38
  </div>
32
39
  }
33
-
40
+
41
+
34
42
  <!-- Header normal -->
35
43
  @if (!draggable) {
36
44
  <div class="flex p-1 pl-4 pr-4 justify-between items-center bg-primary dark:bg-dark-primary border-b border-border dark:border-dark-border rounded-[10px] font-semibold text-2sm cursor-normal select-none">
37
45
  <h3 class="text-[1em] font-semibold text-white leading-none">{{ title }}</h3>
38
- <button
39
- type="button"
46
+ <button
47
+ type="button"
40
48
  (click)="onClose()"
41
49
  class="p-2 rounded-full border border-border dark:border-dark-border text-white hover:bg-dark-background focus:outline-none cursor-pointer"
42
50
  >
@@ -47,22 +55,19 @@
47
55
 
48
56
  <!-- Content -->
49
57
  <div class="m-2"
50
- [ngClass]="{ 'jdialog-full': isFullScreen() }"
51
- [ngStyle]="{
52
- width: getModalWidth(),
53
- height: getModalHeight(),
54
- 'min-width': !isFullScreen() ? '200px' : null,
55
- 'min-height': !isFullScreen() ? '40px' : null
56
- }"
58
+ [ngClass]="{ 'jdialog-full': isFullScreen() }"
59
+ [ngStyle]="{
60
+ height: getModalHeight()
61
+ }"
57
62
  >
58
63
  @if (dialogTemplate) {
59
64
  <ng-container [ngTemplateOutlet]="dialogTemplate"></ng-container>
60
65
  }
61
66
  </div>
62
-
67
+
68
+
63
69
  </div>
64
70
 
65
71
  </div>
66
72
 
67
73
  }
68
-
@@ -123,8 +123,8 @@ export class JDialogComponent implements OnChanges {
123
123
  * Handles the escape key press event.
124
124
  * @param event The keyboard event.
125
125
  */
126
- @HostListener('document:keydown.escape', ['$event'])
127
- handleEscape(event: KeyboardEvent) {
126
+ @HostListener('document:keydown.escape')
127
+ handleEscape() {
128
128
  if (this.openModal) {
129
129
  this.onClose();
130
130
  }
@@ -171,7 +171,7 @@ export class JDialogComponent implements OnChanges {
171
171
  /**
172
172
  * Starts the drag operation.
173
173
  * @param event The mouse event.
174
- * @returns
174
+ * @returns
175
175
  */
176
176
  startDrag(event: MouseEvent) {
177
177
  if (!this.draggable) return;
@@ -1,4 +1,3 @@
1
-
2
1
  import { CommonModule } from '@angular/common';
3
2
  import { Component, CUSTOM_ELEMENTS_SCHEMA, EventEmitter, HostListener, Input, Output, TemplateRef } from '@angular/core';
4
3
  import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -38,13 +37,13 @@ export class JSidebarFormComponent {
38
37
 
39
38
  titleForm: string = 'REGISTRO';
40
39
  @Input() typeForm: FormType = 'none';
41
-
40
+
42
41
  @Input() checkboxes: DynamicCheckbox[] = [];
43
42
  @Input() size: 'small' | 'medium' | 'large' | 'xlarge' = 'medium';
44
43
  @Input() bgColor: string = 'bg-white dark:bg-foreground';
45
44
  @Input() style: { [key: string]: any } = {};
46
45
  @Input() isLoading: boolean = false;
47
-
46
+
48
47
  constructor(public readonly iconsService: JIconsService) { }
49
48
 
50
49
  onSubmit() {
@@ -55,8 +54,8 @@ export class JSidebarFormComponent {
55
54
  this.closeForm.emit()
56
55
  }
57
56
 
58
- @HostListener('document:keydown.escape', ['$event'])
59
- handleEscape(event: KeyboardEvent) {
57
+ @HostListener('document:keydown.escape')
58
+ handleEscape() {
60
59
  if (this.openForm) {
61
60
  this.onClose();
62
61
  }
@@ -47,7 +47,7 @@
47
47
  <table class="min-w-full bg-white dark:bg-dark-background rounded">
48
48
  <thead class="bg-primary dark:bg-dark-primary text-white dark:text-white select-none h-[50px]">
49
49
  <tr>
50
-
50
+
51
51
  <!-- Counter column -->
52
52
  <th
53
53
  class="px-4 py-2 text-center text-xs font-medium text-white uppercase tracking-wider border-b border-border dark:border-dark-border font-bold">
@@ -93,15 +93,15 @@
93
93
  <!-- Actions column - Sticky header -->
94
94
  @if (isOptions) {
95
95
  <th class="!sticky !right-0 bg-primary dark:bg-dark-primary min-w-[50px] px-4 py-2 text-center text-xs font-medium text-white uppercase tracking-wider border-b border-border dark:border-dark-border font-bold shadow-[-4px_0_5px_rgba(0,0,0,0.1)] z-1">
96
-
96
+
97
97
  <span class="text-[10px] opacity-80 border border-border dark:border-dark-border p-2 pl-3 pr-3 rounded-full">Opciones</span>
98
-
98
+
99
99
  <!-- Pseudoelemento para el borde central -->
100
100
  <div class="absolute top-[15px] bottom-[15px] left-0 w-[1px] bg-border dark:bg-dark-border"></div>
101
101
 
102
102
  </th>
103
103
  }
104
-
104
+
105
105
  </tr>
106
106
  </thead>
107
107
  <tbody class="bg-white dark:bg-foreground text-black dark:text-white">
@@ -240,8 +240,9 @@
240
240
  <tr>
241
241
  <td [attr.colspan]="columns.length + 1"
242
242
  class="px-4 py-8 text-center text-sm text-black dark:text-white">
243
- <div class="flex flex-col gap-3 items-center justify-center py-4">
244
- <p>No hay datos disponibles</p>
243
+ <div class="absolute inset-0 flex flex-col gap-3 items-center justify-center py-4 bg-white/80 dark:bg-foreground/80 backdrop-blur-sm z-501 select-none rounded">
244
+ <lucide-icon [name]="iconsService.icons.info" size="35" class="text-primary"></lucide-icon>
245
+ <p class="text-black dark:text-white">No hay datos disponibles</p>
245
246
  </div>
246
247
  </td>
247
248
  </tr>
@@ -250,4 +251,4 @@
250
251
  </table>
251
252
  </div>
252
253
  </div>
253
- </div>
254
+ </div>
@@ -545,7 +545,7 @@
545
545
  <td [attr.colspan]="getVisibleColumnsCount() + 3"
546
546
  class="px-4 py-8 text-center text-sm text-black dark:text-white">
547
547
  <div class="flex flex-col gap-3 items-center justify-center py-4">
548
- <lucide-icon [name]="iconsService.icons.loading" size="30" class="text-primary animate-spin"></lucide-icon>
548
+ <lucide-icon [name]="iconsService.icons.info" size="30" class="text-primary"></lucide-icon>
549
549
  <p>No hay datos disponibles</p>
550
550
  </div>
551
551
  </td>
@@ -13,7 +13,7 @@ import { JButtonComponent } from '../../button/button.component';
13
13
  import { JInputCheckboxComponent } from '../../checkbox/checkbox-input/input-checkbox.component';
14
14
  import { JOptionsTableMenuComponent } from '../../menu/menu-options-table/options-table-menu.component';
15
15
  import { JDialogComponent } from '../../dialog/dialog.component';
16
- import { JViewerImageComponent } from '../../image/image-viewer/viewer-image.component';
16
+ import { JViewerImageComponent } from '../../viewer/image-viewer/viewer-image.component';
17
17
  import { JDropdownSelectComponent } from '../../select/select-dropdown/dropdown-select.component';
18
18
  import { JInputComponent } from '../../input/input/input.component';
19
19
 
@@ -76,7 +76,7 @@ export class JCompleteCrudTableComponent implements OnInit {
76
76
  @Input() optionsType: 'button' | 'dropdown' = 'button';
77
77
  @Input() isOptions: boolean = true;
78
78
 
79
- // Expansion
79
+ // Expansion
80
80
  expandTemplate?: (row: any) => string;
81
81
  expandedRows: Set<any> = new Set();
82
82
 
@@ -1049,4 +1049,4 @@ export class JCompleteCrudTableComponent implements OnInit {
1049
1049
 
1050
1050
 
1051
1051
 
1052
- }
1052
+ }
@@ -1,62 +1,65 @@
1
1
  <div class="relative w-full h-full" #container>
2
2
 
3
- <div class="absolute flex flex-col gap-1 z-2" [ngClass]="{ 'top-3 left-3': isFullscreen, 'top-0 left-0': !isFullscreen }">
3
+ <div class="absolute flex gap-1 z-2" [ngClass]="{ 'top-3 left-3': isFullscreen, 'top-0 left-0': !isFullscreen }">
4
4
  <JButton
5
- (clicked)="reset()"
5
+ [icon]="iconsService.icons.rotateLeft"
6
+ [iconSize]="20"
7
+ (clicked)="rotateLeftImg()"
6
8
  classes="secondary w-[35px] h-[35px]"
7
- >
8
- <lucide-icon [name]="iconsService.icons.reset" size="20" />
9
- </JButton>
9
+ />
10
10
 
11
11
  <JButton
12
- (clicked)="rotateLeftImg()"
12
+ [icon]="iconsService.icons.reset"
13
+ [iconSize]="20"
14
+ (clicked)="reset()"
13
15
  classes="secondary w-[35px] h-[35px]"
14
- >
15
- <lucide-icon [name]="iconsService.icons.rotateLeft" size="20" />
16
- </JButton>
16
+ />
17
+ </div>
17
18
 
19
+
20
+ <div class="absolute flex flex-col gap-1 z-2" [ngClass]="{ 'top-3 left-3': isFullscreen, 'top-10 left-0': !isFullscreen }">
18
21
  <JButton
22
+ [icon]="iconsService.icons.rotateRight"
23
+ [iconSize]="20"
19
24
  (clicked)="rotateRightImg()"
20
25
  classes="secondary w-[35px] h-[35px]"
21
- >
22
- <lucide-icon [name]="iconsService.icons.rotateRight" size="20" />
23
- </JButton>
26
+ />
24
27
  </div>
25
28
 
26
29
 
27
30
  <div class="absolute flex gap-1 z-2" [ngClass]="{ 'top-3 right-3': isFullscreen, 'top-0 right-0': !isFullscreen }">
28
31
  <JButton
29
- [disabled]="zoom === 3"
32
+ [icon]="iconsService.icons.download"
33
+ [iconSize]="20"
30
34
  (clicked)="download()"
35
+ [isLoading]="isDownloading"
31
36
  classes="secondary w-[35px] h-[35px]"
32
- >
33
- <lucide-icon [name]="iconsService.icons.download" size="20" />
34
- </JButton>
37
+ />
35
38
 
36
39
  <JButton
40
+ [icon]="isFullscreen ? iconsService.icons.exitFullscreen : iconsService.icons.fullscreen"
41
+ [iconSize]="20"
37
42
  (clicked)="toggleFullscreen(container)"
38
43
  classes="secondary w-[35px] h-[35px]"
39
- >
40
- @if (isFullscreen) {
41
- <lucide-icon [name]="iconsService.icons.exitFullscreen" size="20" />
42
- } @else {
43
- <lucide-icon [name]="iconsService.icons.fullscreen" size="20" />
44
- }
45
- </JButton>
44
+ />
46
45
  </div>
47
46
 
48
47
  <div class="absolute flex flex-col gap-1 z-2" [ngClass]="{ 'top-13 right-3': isFullscreen, 'top-10 right-0': !isFullscreen }">
49
48
  <JButton
49
+ [icon]="iconsService.icons.zoomIn"
50
+ [iconSize]="20"
50
51
  [disabled]="zoom === 3"
51
52
  (clicked)="zoomIn()"
52
53
  classes="secondary w-[35px] h-[35px]"
53
- >
54
- <lucide-icon [name]="iconsService.icons.zoomIn" size="20" />
55
- </JButton>
54
+ />
56
55
 
57
- <JButton [disabled]="zoom === 0.5" (clicked)="zoomOut()" classes="secondary w-[35px] h-[35px]">
58
- <lucide-icon [name]="iconsService.icons.zoomOut" size="20" />
59
- </JButton>
56
+ <JButton
57
+ [icon]="iconsService.icons.zoomOut"
58
+ [iconSize]="20"
59
+ [disabled]="zoom === 0.5"
60
+ (clicked)="zoomOut()"
61
+ classes="secondary w-[35px] h-[35px]"
62
+ />
60
63
  </div>
61
64
 
62
65
 
@@ -6,12 +6,12 @@ import { JIconsService } from 'tailjng';
6
6
  import { JButtonComponent } from '../../button/button.component';
7
7
 
8
8
  @Component({
9
- selector: 'JViewerImage',
9
+ selector: 'JImageViewer',
10
10
  imports: [CommonModule, LucideAngularModule, JButtonComponent],
11
- templateUrl: './viewer-image.component.html',
12
- styleUrl: './viewer-image.component.css'
11
+ templateUrl: './image-viewer.component.html',
12
+ styleUrl: './image-viewer.component.css'
13
13
  })
14
- export class JViewerImageComponent implements OnChanges {
14
+ export class JImageViewerComponent implements OnChanges {
15
15
 
16
16
  @Input() src!: string | SafeUrl;
17
17
  @Input() alt: string = 'Imagen';
@@ -86,50 +86,80 @@ export class JViewerImageComponent implements OnChanges {
86
86
  /**
87
87
  * Downloads the currently displayed image.
88
88
  */
89
+ isDownloading = false;
90
+
91
+ private getImageUrl(): string {
92
+ if (typeof this.src === 'string') {
93
+ return this.src;
94
+ }
95
+
96
+ // SafeUrlImpl → sacar la propiedad interna
97
+ const anySrc = this.src as any;
98
+ if (anySrc.changingThisBreaksApplicationSecurity) {
99
+ return anySrc.changingThisBreaksApplicationSecurity as string;
100
+ }
101
+
102
+ // fallback (por si acaso)
103
+ return this.src.toString();
104
+ }
105
+
89
106
  download() {
90
107
  try {
91
108
  if (!this.src) return;
92
109
 
93
- // Convierte SafeUrl a string si es necesario
94
- const imageUrl = this.src.toString();
110
+ this.isDownloading = true;
95
111
 
96
- // Nombre de archivo predeterminado
112
+ const imageUrl = this.getImageUrl();
97
113
  const fileName = this.alt?.replace(/\s+/g, '') || 'imagen';
98
114
 
99
- // Detecta si es base64 o URL
115
+ // Si es base64
100
116
  if (imageUrl.startsWith('data:image')) {
101
- // Base64 → descarga directa
102
117
  const link = document.createElement('a');
103
118
  link.href = imageUrl;
104
119
  link.download = `${fileName}.png`;
105
120
  document.body.appendChild(link);
106
121
  link.click();
107
122
  document.body.removeChild(link);
108
- } else {
109
- // Descarga vía fetch para evitar CORS si es necesario
110
- fetch(imageUrl)
111
- .then((response) => response.blob())
112
- .then((blob) => {
113
- const url = URL.createObjectURL(blob);
114
- const link = document.createElement('a');
115
- link.href = url;
116
- link.download = `${fileName}.${blob.type.split('/')[1] || 'png'}`;
117
- document.body.appendChild(link);
118
- link.click();
119
- document.body.removeChild(link);
120
- URL.revokeObjectURL(url);
121
- })
122
- .catch((error) => {
123
- console.error('Error al descargar la imagen:', error);
124
- });
123
+ this.isDownloading = false;
124
+ return;
125
125
  }
126
+
127
+ // Si es URL normal
128
+ fetch(imageUrl)
129
+ .then((response) => {
130
+ if (!response.ok) {
131
+ throw new Error(`HTTP ${response.status}`);
132
+ }
133
+ return response.blob();
134
+ })
135
+ .then((blob) => {
136
+ const url = URL.createObjectURL(blob);
137
+ const link = document.createElement('a');
138
+ const ext = blob.type.split('/')[1] || 'png';
139
+
140
+ link.href = url;
141
+ link.download = `${fileName}.${ext}`;
142
+ document.body.appendChild(link);
143
+ link.click();
144
+ document.body.removeChild(link);
145
+ URL.revokeObjectURL(url);
146
+ })
147
+ .catch((error) => {
148
+ console.error('Error al descargar la imagen:', error);
149
+ })
150
+ .finally(() => {
151
+ this.isDownloading = false;
152
+ });
153
+
126
154
  } catch (err) {
155
+ this.isDownloading = false;
127
156
  console.error('Error general en download():', err);
128
157
  }
129
158
  }
130
159
 
131
160
 
132
161
 
162
+
133
163
  /**
134
164
  * Zooms in the image.
135
165
  */
@@ -0,0 +1,28 @@
1
+ <div class="flex-1 flex flex-col items-center justify-center">
2
+ <object
3
+ [data]="safeUrl"
4
+ type="application/pdf"
5
+ class="w-full h-full min-h-[300px] md:h-[75vh] overflow-auto"
6
+ >
7
+ <div class="p-6 flex flex-col items-center justify-center text-center gap-3">
8
+ <!-- Icono -->
9
+ <lucide-icon [name]="icons.circleAlert" size="35" class="text-primary"></lucide-icon>
10
+
11
+ <!-- Texto -->
12
+ <div class="space-y-1">
13
+ <h3 class="text-sm font-semibold text-slate-800 dark:text-slate-100">
14
+ No se pudo mostrar el PDF
15
+ </h3>
16
+ <p class="text-xs text-slate-500 dark:text-slate-400 max-w-xs">
17
+ Tu navegador no es compatible con la visualización embebida de este documento.
18
+ Puedes descargarlo y verlo en tu visor de PDF preferido.
19
+ </p>
20
+ </div>
21
+
22
+ <!-- Botón -->
23
+ <a [href]="safeUrl" target="_blank" class="inline-flex items-center justify-center px-4 py-2 rounded-full text-xs font-medium bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 transition-colors">
24
+ Descargar archivo
25
+ </a>
26
+ </div>
27
+ </object>
28
+ </div>
@@ -0,0 +1,31 @@
1
+ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2
+ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
3
+ import { CircleAlert, LucideAngularModule } from 'lucide-angular';
4
+
5
+ @Component({
6
+ selector: 'JViewerPdf',
7
+ standalone: true,
8
+ imports: [LucideAngularModule],
9
+ templateUrl: './pdf-viewer.component.html',
10
+ styleUrls: ['./pdf-viewer.component.scss']
11
+ })
12
+ export class JViewerPdfComponent implements OnChanges {
13
+
14
+ icons = {
15
+ circleAlert: CircleAlert
16
+ };
17
+
18
+ // URLs normales (strings) para usarlas en <a href="">
19
+ @Input() url = 'http://example/example.pdf';
20
+
21
+ // URLs “seguras” para <object>
22
+ safeUrl: SafeResourceUrl | null = null;
23
+
24
+ constructor(private sanitizer: DomSanitizer) { }
25
+
26
+ ngOnChanges(changes: SimpleChanges): void {
27
+ if (changes['url']) {
28
+ this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.url);
29
+ }
30
+ }
31
+ }