@caribeng/ui 22.1.0 → 22.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +52 -12
  2. package/fesm2022/caribeng-ui-accordion.mjs +2 -2
  3. package/fesm2022/caribeng-ui-anchor.mjs +169 -0
  4. package/fesm2022/caribeng-ui-autocomplete.mjs +10 -8
  5. package/fesm2022/caribeng-ui-avatargroup.mjs +9 -3
  6. package/fesm2022/caribeng-ui-blockui.mjs +12 -4
  7. package/fesm2022/caribeng-ui-breadcrumb.mjs +5 -2
  8. package/fesm2022/caribeng-ui-button.mjs +22 -2
  9. package/fesm2022/caribeng-ui-caribe-alert.mjs +5 -2
  10. package/fesm2022/caribeng-ui-caribe-animation-lottie.mjs +6 -3
  11. package/fesm2022/caribeng-ui-caribe-button-group.mjs +6 -3
  12. package/fesm2022/caribeng-ui-caribe-calendar.mjs +11 -6
  13. package/fesm2022/caribeng-ui-caribe-card-content.mjs +6 -3
  14. package/fesm2022/caribeng-ui-caribe-card-move.mjs +29 -7
  15. package/fesm2022/caribeng-ui-caribe-card-select.mjs +56 -14
  16. package/fesm2022/caribeng-ui-caribe-circle-loading.mjs +10 -7
  17. package/fesm2022/caribeng-ui-caribe-coachmark.mjs +42 -19
  18. package/fesm2022/caribeng-ui-caribe-data-table.mjs +15 -9
  19. package/fesm2022/caribeng-ui-caribe-dots-indicator.mjs +38 -5
  20. package/fesm2022/caribeng-ui-caribe-expansion-panel.mjs +16 -5
  21. package/fesm2022/caribeng-ui-caribe-fab-menu.mjs +56 -7
  22. package/fesm2022/caribeng-ui-caribe-file-download.mjs +68 -27
  23. package/fesm2022/caribeng-ui-caribe-file-select.mjs +22 -17
  24. package/fesm2022/caribeng-ui-caribe-float-menu.mjs +6 -3
  25. package/fesm2022/caribeng-ui-caribe-footer.mjs +8 -4
  26. package/fesm2022/caribeng-ui-caribe-header.mjs +6 -3
  27. package/fesm2022/caribeng-ui-caribe-icon-button.mjs +6 -3
  28. package/fesm2022/caribeng-ui-caribe-illustration.mjs +6 -3
  29. package/fesm2022/caribeng-ui-caribe-inline-alert.mjs +5 -2
  30. package/fesm2022/caribeng-ui-caribe-input-search.mjs +14 -10
  31. package/fesm2022/caribeng-ui-caribe-input-select.mjs +12 -10
  32. package/fesm2022/caribeng-ui-caribe-input-token.mjs +61 -11
  33. package/fesm2022/caribeng-ui-caribe-link.mjs +7 -2
  34. package/fesm2022/caribeng-ui-caribe-loader.mjs +8 -5
  35. package/fesm2022/caribeng-ui-caribe-modal.mjs +32 -16
  36. package/fesm2022/caribeng-ui-caribe-off-canvas.mjs +26 -12
  37. package/fesm2022/caribeng-ui-caribe-onboarding.mjs +44 -19
  38. package/fesm2022/caribeng-ui-caribe-pictogram.mjs +6 -3
  39. package/fesm2022/caribeng-ui-caribe-pop-over.mjs +34 -10
  40. package/fesm2022/caribeng-ui-caribe-preloader.mjs +9 -6
  41. package/fesm2022/caribeng-ui-caribe-progress-bar.mjs +5 -2
  42. package/fesm2022/caribeng-ui-caribe-shortcut.mjs +8 -3
  43. package/fesm2022/caribeng-ui-caribe-step-indicator.mjs +12 -5
  44. package/fesm2022/caribeng-ui-caribe-stroke.mjs +6 -3
  45. package/fesm2022/caribeng-ui-caribe-switch.mjs +5 -3
  46. package/fesm2022/caribeng-ui-caribe-tag-button.mjs +1 -1
  47. package/fesm2022/caribeng-ui-caribe-tracker.mjs +5 -2
  48. package/fesm2022/caribeng-ui-caribe-transaction-status.mjs +1 -1
  49. package/fesm2022/caribeng-ui-caribe-voucher.mjs +11 -8
  50. package/fesm2022/caribeng-ui-carousel.mjs +49 -14
  51. package/fesm2022/caribeng-ui-cascadeselect.mjs +22 -8
  52. package/fesm2022/caribeng-ui-chart.mjs +2598 -303
  53. package/fesm2022/caribeng-ui-chip.mjs +9 -4
  54. package/fesm2022/caribeng-ui-chips.mjs +9 -5
  55. package/fesm2022/caribeng-ui-colorpicker.mjs +9 -7
  56. package/fesm2022/caribeng-ui-confirmdialog.mjs +74 -9
  57. package/fesm2022/caribeng-ui-confirmpopup.mjs +134 -9
  58. package/fesm2022/caribeng-ui-contextmenu.mjs +6 -3
  59. package/fesm2022/caribeng-ui-dataview.mjs +211 -6
  60. package/fesm2022/caribeng-ui-datepicker.mjs +1630 -393
  61. package/fesm2022/caribeng-ui-descriptions.mjs +124 -0
  62. package/fesm2022/caribeng-ui-dialog.mjs +10 -7
  63. package/fesm2022/caribeng-ui-dock.mjs +6 -3
  64. package/fesm2022/caribeng-ui-drawer.mjs +10 -7
  65. package/fesm2022/caribeng-ui-dynamicdialog.mjs +210 -6
  66. package/fesm2022/caribeng-ui-editor.mjs +4392 -243
  67. package/fesm2022/caribeng-ui-empty.mjs +80 -0
  68. package/fesm2022/caribeng-ui-file-card.mjs +2 -2
  69. package/fesm2022/caribeng-ui-fileupload.mjs +96 -19
  70. package/fesm2022/caribeng-ui-floatlabel.mjs +4 -2
  71. package/fesm2022/caribeng-ui-folder-card.mjs +10 -3
  72. package/fesm2022/caribeng-ui-galleria.mjs +76 -45
  73. package/fesm2022/caribeng-ui-image.mjs +52 -29
  74. package/fesm2022/caribeng-ui-inplace.mjs +22 -6
  75. package/fesm2022/caribeng-ui-input-number.mjs +9 -7
  76. package/fesm2022/caribeng-ui-inputgroup.mjs +2 -2
  77. package/fesm2022/caribeng-ui-inputmask.mjs +14 -11
  78. package/fesm2022/caribeng-ui-inputotp.mjs +12 -6
  79. package/fesm2022/caribeng-ui-keyfilter.mjs +72 -0
  80. package/fesm2022/caribeng-ui-listbox.mjs +13 -11
  81. package/fesm2022/caribeng-ui-megamenu.mjs +222 -30
  82. package/fesm2022/caribeng-ui-menu.mjs +11 -5
  83. package/fesm2022/caribeng-ui-menubar.mjs +10 -5
  84. package/fesm2022/caribeng-ui-message.mjs +7 -4
  85. package/fesm2022/caribeng-ui-metergroup.mjs +29 -11
  86. package/fesm2022/caribeng-ui-multiselect.mjs +399 -97
  87. package/fesm2022/caribeng-ui-orderlist.mjs +230 -11
  88. package/fesm2022/caribeng-ui-organizationchart.mjs +12 -7
  89. package/fesm2022/caribeng-ui-paginator.mjs +18 -15
  90. package/fesm2022/caribeng-ui-panel.mjs +5 -2
  91. package/fesm2022/caribeng-ui-panelmenu.mjs +6 -3
  92. package/fesm2022/caribeng-ui-password.mjs +8 -6
  93. package/fesm2022/caribeng-ui-pdfviewer.mjs +1553 -0
  94. package/fesm2022/caribeng-ui-picklist.mjs +251 -14
  95. package/fesm2022/caribeng-ui-pricing.mjs +17 -14
  96. package/fesm2022/caribeng-ui-rating.mjs +7 -3
  97. package/fesm2022/caribeng-ui-repeater.mjs +223 -0
  98. package/fesm2022/caribeng-ui-result.mjs +91 -0
  99. package/fesm2022/caribeng-ui-scrollpanel.mjs +6 -3
  100. package/fesm2022/caribeng-ui-scrolltop.mjs +6 -3
  101. package/fesm2022/caribeng-ui-select.mjs +379 -87
  102. package/fesm2022/caribeng-ui-selectbutton.mjs +2 -2
  103. package/fesm2022/caribeng-ui-slider.mjs +171 -75
  104. package/fesm2022/caribeng-ui-speeddial.mjs +7 -4
  105. package/fesm2022/caribeng-ui-spinner.mjs +5 -2
  106. package/fesm2022/caribeng-ui-splitbutton.mjs +163 -0
  107. package/fesm2022/caribeng-ui-splitter.mjs +5 -2
  108. package/fesm2022/caribeng-ui-statistic.mjs +183 -0
  109. package/fesm2022/caribeng-ui-stepper.mjs +8 -5
  110. package/fesm2022/caribeng-ui-steps.mjs +6 -3
  111. package/fesm2022/caribeng-ui-table.mjs +708 -82
  112. package/fesm2022/caribeng-ui-tabmenu.mjs +6 -3
  113. package/fesm2022/caribeng-ui-tag.mjs +2 -2
  114. package/fesm2022/caribeng-ui-terminal.mjs +45 -9
  115. package/fesm2022/caribeng-ui-themetoggle.mjs +82 -0
  116. package/fesm2022/caribeng-ui-tieredmenu.mjs +9 -6
  117. package/fesm2022/caribeng-ui-timeline.mjs +175 -6
  118. package/fesm2022/caribeng-ui-toast.mjs +7 -4
  119. package/fesm2022/caribeng-ui-togglebutton.mjs +8 -6
  120. package/fesm2022/caribeng-ui-toolbar.mjs +6 -3
  121. package/fesm2022/caribeng-ui-tree.mjs +302 -6
  122. package/fesm2022/caribeng-ui-treeselect.mjs +26 -10
  123. package/fesm2022/caribeng-ui-treetable.mjs +269 -6
  124. package/fesm2022/caribeng-ui-virtualscroller.mjs +123 -10
  125. package/fesm2022/caribeng-ui.mjs +829 -6
  126. package/package.json +70 -11
  127. package/schematics/collection.json +10 -0
  128. package/schematics/ng-add/index.js +125 -0
  129. package/schematics/ng-add/schema.js +2 -0
  130. package/schematics/ng-add/schema.json +66 -0
  131. package/styles/caribeng.css +380 -0
  132. package/styles/scss/_mixins.scss +46 -0
  133. package/styles/scss/_overlay.scss +15 -0
  134. package/styles/scss/_tokens.scss +267 -0
  135. package/styles/scss/_utilities.scss +73 -0
  136. package/styles/scss/_variables.scss +33 -0
  137. package/styles/scss/index.scss +16 -0
  138. package/types/caribeng-ui-anchor.d.ts +50 -0
  139. package/types/caribeng-ui-autocomplete.d.ts +4 -2
  140. package/types/caribeng-ui-avatargroup.d.ts +3 -1
  141. package/types/caribeng-ui-blockui.d.ts +9 -4
  142. package/types/caribeng-ui-breadcrumb.d.ts +3 -0
  143. package/types/caribeng-ui-button.d.ts +9 -1
  144. package/types/caribeng-ui-caribe-alert.d.ts +3 -0
  145. package/types/caribeng-ui-caribe-animation-lottie.d.ts +4 -1
  146. package/types/caribeng-ui-caribe-button-group.d.ts +4 -1
  147. package/types/caribeng-ui-caribe-calendar.d.ts +5 -1
  148. package/types/caribeng-ui-caribe-card-content.d.ts +4 -1
  149. package/types/caribeng-ui-caribe-card-move.d.ts +13 -13
  150. package/types/caribeng-ui-caribe-card-select.d.ts +10 -1
  151. package/types/caribeng-ui-caribe-circle-loading.d.ts +4 -1
  152. package/types/caribeng-ui-caribe-coachmark.d.ts +6 -0
  153. package/types/caribeng-ui-caribe-data-table.d.ts +7 -2
  154. package/types/caribeng-ui-caribe-dots-indicator.d.ts +9 -0
  155. package/types/caribeng-ui-caribe-expansion-panel.d.ts +7 -10
  156. package/types/caribeng-ui-caribe-fab-menu.d.ts +10 -1
  157. package/types/caribeng-ui-caribe-file-download.d.ts +8 -10
  158. package/types/caribeng-ui-caribe-file-select.d.ts +7 -1
  159. package/types/caribeng-ui-caribe-float-menu.d.ts +4 -1
  160. package/types/caribeng-ui-caribe-header.d.ts +3 -0
  161. package/types/caribeng-ui-caribe-icon-button.d.ts +4 -1
  162. package/types/caribeng-ui-caribe-illustration.d.ts +4 -1
  163. package/types/caribeng-ui-caribe-inline-alert.d.ts +3 -0
  164. package/types/caribeng-ui-caribe-input-search.d.ts +4 -2
  165. package/types/caribeng-ui-caribe-input-select.d.ts +4 -2
  166. package/types/caribeng-ui-caribe-input-token.d.ts +16 -1
  167. package/types/caribeng-ui-caribe-link.d.ts +3 -0
  168. package/types/caribeng-ui-caribe-loader.d.ts +4 -1
  169. package/types/caribeng-ui-caribe-modal.d.ts +7 -10
  170. package/types/caribeng-ui-caribe-off-canvas.d.ts +6 -1
  171. package/types/caribeng-ui-caribe-onboarding.d.ts +6 -0
  172. package/types/caribeng-ui-caribe-pictogram.d.ts +4 -1
  173. package/types/caribeng-ui-caribe-pop-over.d.ts +7 -10
  174. package/types/caribeng-ui-caribe-preloader.d.ts +4 -1
  175. package/types/caribeng-ui-caribe-progress-bar.d.ts +3 -0
  176. package/types/caribeng-ui-caribe-shortcut.d.ts +3 -1
  177. package/types/caribeng-ui-caribe-step-indicator.d.ts +6 -0
  178. package/types/caribeng-ui-caribe-stroke.d.ts +4 -1
  179. package/types/caribeng-ui-caribe-switch.d.ts +3 -1
  180. package/types/caribeng-ui-caribe-tracker.d.ts +3 -0
  181. package/types/caribeng-ui-caribe-voucher.d.ts +5 -2
  182. package/types/caribeng-ui-carousel.d.ts +10 -1
  183. package/types/caribeng-ui-cascadeselect.d.ts +7 -2
  184. package/types/caribeng-ui-chart.d.ts +469 -44
  185. package/types/caribeng-ui-chip.d.ts +6 -0
  186. package/types/caribeng-ui-chips.d.ts +6 -2
  187. package/types/caribeng-ui-colorpicker.d.ts +3 -1
  188. package/types/caribeng-ui-confirmdialog.d.ts +25 -6
  189. package/types/caribeng-ui-confirmpopup.d.ts +37 -5
  190. package/types/caribeng-ui-contextmenu.d.ts +4 -1
  191. package/types/caribeng-ui-dataview.d.ts +63 -5
  192. package/types/caribeng-ui-datepicker.d.ts +186 -21
  193. package/types/caribeng-ui-descriptions.d.ts +63 -0
  194. package/types/caribeng-ui-dialog.d.ts +4 -1
  195. package/types/caribeng-ui-dock.d.ts +4 -1
  196. package/types/caribeng-ui-drawer.d.ts +4 -1
  197. package/types/caribeng-ui-dynamicdialog.d.ts +93 -3
  198. package/types/caribeng-ui-editor.d.ts +486 -13
  199. package/types/caribeng-ui-empty.d.ts +34 -0
  200. package/types/caribeng-ui-file-card.d.ts +1 -1
  201. package/types/caribeng-ui-fileupload.d.ts +27 -4
  202. package/types/caribeng-ui-folder-card.d.ts +4 -1
  203. package/types/caribeng-ui-galleria.d.ts +9 -0
  204. package/types/caribeng-ui-image.d.ts +10 -0
  205. package/types/caribeng-ui-inplace.d.ts +8 -0
  206. package/types/caribeng-ui-input-number.d.ts +3 -1
  207. package/types/caribeng-ui-inputmask.d.ts +5 -0
  208. package/types/caribeng-ui-inputotp.d.ts +7 -2
  209. package/types/caribeng-ui-keyfilter.d.ts +30 -0
  210. package/types/caribeng-ui-listbox.d.ts +5 -3
  211. package/types/caribeng-ui-megamenu.d.ts +26 -2
  212. package/types/caribeng-ui-menu.d.ts +7 -2
  213. package/types/caribeng-ui-menubar.d.ts +5 -1
  214. package/types/caribeng-ui-message.d.ts +3 -0
  215. package/types/caribeng-ui-metergroup.d.ts +9 -1
  216. package/types/caribeng-ui-multiselect.d.ts +71 -10
  217. package/types/caribeng-ui-orderlist.d.ts +58 -6
  218. package/types/caribeng-ui-organizationchart.d.ts +7 -1
  219. package/types/caribeng-ui-paginator.d.ts +3 -0
  220. package/types/caribeng-ui-panel.d.ts +3 -0
  221. package/types/caribeng-ui-panelmenu.d.ts +4 -1
  222. package/types/caribeng-ui-password.d.ts +4 -2
  223. package/types/caribeng-ui-pdfviewer.d.ts +258 -0
  224. package/types/caribeng-ui-picklist.d.ts +67 -7
  225. package/types/caribeng-ui-pricing.d.ts +8 -5
  226. package/types/caribeng-ui-rating.d.ts +6 -2
  227. package/types/caribeng-ui-repeater.d.ts +79 -0
  228. package/types/caribeng-ui-result.d.ts +35 -0
  229. package/types/caribeng-ui-scrollpanel.d.ts +4 -1
  230. package/types/caribeng-ui-scrolltop.d.ts +4 -1
  231. package/types/caribeng-ui-select.d.ts +73 -9
  232. package/types/caribeng-ui-slider.d.ts +63 -10
  233. package/types/caribeng-ui-speeddial.d.ts +5 -2
  234. package/types/caribeng-ui-spinner.d.ts +3 -0
  235. package/types/caribeng-ui-splitbutton.d.ts +49 -0
  236. package/types/caribeng-ui-splitter.d.ts +3 -0
  237. package/types/caribeng-ui-statistic.d.ts +65 -0
  238. package/types/caribeng-ui-stepper.d.ts +3 -0
  239. package/types/caribeng-ui-steps.d.ts +4 -1
  240. package/types/caribeng-ui-table.d.ts +184 -16
  241. package/types/caribeng-ui-tabmenu.d.ts +4 -1
  242. package/types/caribeng-ui-terminal.d.ts +10 -0
  243. package/types/caribeng-ui-themetoggle.d.ts +32 -0
  244. package/types/caribeng-ui-tieredmenu.d.ts +5 -2
  245. package/types/caribeng-ui-timeline.d.ts +63 -4
  246. package/types/caribeng-ui-toast.d.ts +3 -0
  247. package/types/caribeng-ui-togglebutton.d.ts +5 -3
  248. package/types/caribeng-ui-toolbar.d.ts +7 -4
  249. package/types/caribeng-ui-tree.d.ts +76 -4
  250. package/types/caribeng-ui-treeselect.d.ts +10 -2
  251. package/types/caribeng-ui-treetable.d.ts +68 -4
  252. package/types/caribeng-ui-virtualscroller.d.ts +45 -5
  253. package/types/caribeng-ui.d.ts +501 -12
@@ -0,0 +1,1553 @@
1
+ import { DOCUMENT, isPlatformBrowser } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { InjectionToken, makeEnvironmentProviders, inject, Injectable, input, booleanAttribute, model, output, Injector, PLATFORM_ID, signal, viewChild, viewChildren, computed, effect, untracked, afterNextRender, DestroyRef, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
4
+ import { injectCbTranslation, cbFormat } from '@caribeng/ui';
5
+ import { CbIconRegistry, CbIcon } from '@caribeng/ui/icon';
6
+ import { panelLeft, printer, zoomOut, zoomIn } from '@caribeng/ui/icons';
7
+ import { CbSpinner } from '@caribeng/ui/spinner';
8
+
9
+ const CB_PDF_VIEWER_OPTIONS = new InjectionToken('CB_PDF_VIEWER_OPTIONS');
10
+ /**
11
+ * Configura el visor de PDF (worker, recursos y carga de pdf.js).
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * bootstrapApplication(App, {
16
+ * providers: [provideCaribeNG(), provideCbPdfViewer({ workerSrc: '/static/pdf.worker.min.mjs' })],
17
+ * });
18
+ * ```
19
+ */
20
+ function provideCbPdfViewer(options) {
21
+ return makeEnvironmentProviders([{ provide: CB_PDF_VIEWER_OPTIONS, useValue: options }]);
22
+ }
23
+ /**
24
+ * Carga pdf.js una sola vez y configura su worker. Las URLs se resuelven contra
25
+ * el `<base href>` del documento: el worker de pdf.js las pediría relativas a
26
+ * su propio script.
27
+ */
28
+ class CbPdfEngine {
29
+ constructor() {
30
+ this.options = inject(CB_PDF_VIEWER_OPTIONS, { optional: true }) ?? {};
31
+ this.document = inject(DOCUMENT);
32
+ }
33
+ load() {
34
+ this.pdfjs ??= (this.options.loader ?? (() => import('pdfjs-dist/legacy/build/pdf.mjs')))().then((pdfjs) => {
35
+ if (pdfjs.GlobalWorkerOptions && !pdfjs.GlobalWorkerOptions.workerSrc && !pdfjs.GlobalWorkerOptions.workerPort) {
36
+ pdfjs.GlobalWorkerOptions.workerSrc = this.resolve(this.options.workerSrc ?? 'pdfjs/build/pdf.worker.min.mjs');
37
+ }
38
+ return pdfjs;
39
+ });
40
+ // Si falla (p. ej. pdfjs-dist no está instalado) se puede reintentar.
41
+ this.pdfjs.catch(() => (this.pdfjs = undefined));
42
+ return this.pdfjs;
43
+ }
44
+ resources() {
45
+ const base = this.resolve(withSlash(this.options.assetsUrl ?? 'pdfjs/'));
46
+ return {
47
+ cMapUrl: `${base}cmaps/`,
48
+ standardFontDataUrl: `${base}standard_fonts/`,
49
+ wasmUrl: `${base}wasm/`,
50
+ iccUrl: `${base}iccs/`,
51
+ };
52
+ }
53
+ /**
54
+ * Resuelve una URL contra el `<base href>`, como Angular y `<img>`; pdf.js
55
+ * la resolvería contra la ruta actual (`/components/visor/doc.pdf`).
56
+ */
57
+ resolve(url) {
58
+ try {
59
+ return new URL(url, this.document.baseURI).href;
60
+ }
61
+ catch {
62
+ return String(url);
63
+ }
64
+ }
65
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbPdfEngine, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
66
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbPdfEngine, providedIn: 'root' }); }
67
+ }
68
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbPdfEngine, decorators: [{
69
+ type: Injectable,
70
+ args: [{ providedIn: 'root' }]
71
+ }] });
72
+ function withSlash(url) {
73
+ return url.endsWith('/') ? url : `${url}/`;
74
+ }
75
+
76
+ /** Resolución de impresión (ppp). */
77
+ const PRINT_DPI = 150;
78
+ /**
79
+ * Imprime el documento: rasteriza cada página a {@link PRINT_DPI} ppp en un
80
+ * `<iframe>` oculto y abre el diálogo de impresión del navegador (así se
81
+ * imprime el PDF y no la página de la aplicación).
82
+ */
83
+ async function printPdfDocument(pdf, document, onProgress, rotation = 0) {
84
+ const urls = [];
85
+ const frame = document.createElement('iframe');
86
+ frame.setAttribute('aria-hidden', 'true');
87
+ frame.tabIndex = -1;
88
+ frame.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;visibility:hidden';
89
+ try {
90
+ let pageSize = '';
91
+ for (let n = 1; n <= pdf.numPages; n++) {
92
+ const page = await pdf.getPage(n);
93
+ const viewport = page.getViewport({ scale: PRINT_DPI / 72, rotation: (page.rotate + rotation) % 360 });
94
+ if (n === 1)
95
+ pageSize = `${Math.round((viewport.width * 72) / PRINT_DPI)}pt ${Math.round((viewport.height * 72) / PRINT_DPI)}pt`;
96
+ const canvas = document.createElement('canvas');
97
+ canvas.width = Math.floor(viewport.width);
98
+ canvas.height = Math.floor(viewport.height);
99
+ await page.render({ canvas, viewport, intent: 'print' }).promise;
100
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
101
+ if (blob)
102
+ urls.push(URL.createObjectURL(blob));
103
+ canvas.width = canvas.height = 0;
104
+ page.cleanup();
105
+ onProgress(n, pdf.numPages);
106
+ }
107
+ document.body.append(frame);
108
+ const doc = frame.contentDocument;
109
+ doc.open();
110
+ doc.write(`<!doctype html><html><head><style>@page{size:${pageSize};margin:0}html,body{margin:0}` +
111
+ `img{display:block;width:100%;height:auto;break-after:page}img:last-child{break-after:auto}</style></head><body></body></html>`);
112
+ doc.close();
113
+ await Promise.all(urls.map((src) => {
114
+ const img = doc.createElement('img');
115
+ img.src = src;
116
+ img.alt = '';
117
+ doc.body.append(img);
118
+ return img.decode().catch(() => undefined);
119
+ }));
120
+ const win = frame.contentWindow;
121
+ await new Promise((resolve) => {
122
+ win.addEventListener('afterprint', () => resolve(), { once: true });
123
+ win.focus();
124
+ win.print();
125
+ // Red de seguridad por si el navegador no emite afterprint.
126
+ setTimeout(resolve, 60_000);
127
+ });
128
+ }
129
+ finally {
130
+ frame.remove();
131
+ for (const url of urls)
132
+ URL.revokeObjectURL(url);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Índice de búsqueda de una página. Ignora mayúsculas, tildes y ligaduras
138
+ * ("dirección" encuentra "direccion") y une los espacios seguidos. Solo cuenta
139
+ * los elementos con texto, en el mismo orden que la capa de texto de pdf.js.
140
+ */
141
+ function buildTextIndex(items) {
142
+ let text = '';
143
+ const map = [];
144
+ const push = (char, item, offset) => {
145
+ if (/\s/.test(char)) {
146
+ if (!text || text.endsWith(' '))
147
+ return;
148
+ char = ' ';
149
+ }
150
+ text += char;
151
+ map.push({ item, offset });
152
+ };
153
+ let index = 0;
154
+ for (const it of items) {
155
+ if (it.str === undefined)
156
+ continue;
157
+ const str = it.str;
158
+ for (let offset = 0; offset < str.length; offset++) {
159
+ for (const char of normalizeChar(str[offset]))
160
+ push(char, index, offset);
161
+ }
162
+ if (it.hasEOL)
163
+ push(' ', index, str.length);
164
+ index++;
165
+ }
166
+ return { text, map };
167
+ }
168
+ /** Normaliza el texto buscado igual que el índice. */
169
+ function normalizeQuery(query) {
170
+ return Array.from(query, normalizeChar).join('').replace(/\s+/g, ' ').trim();
171
+ }
172
+ /** Coincidencias de `query` en una página. */
173
+ function findMatches(index, query, page) {
174
+ const q = normalizeQuery(query);
175
+ if (!q)
176
+ return [];
177
+ const matches = [];
178
+ let from = index.text.indexOf(q);
179
+ while (from !== -1) {
180
+ const last = index.map[from + q.length - 1];
181
+ matches.push({ page, start: index.map[from], end: { item: last.item, offset: last.offset + 1 } });
182
+ from = index.text.indexOf(q, from + q.length);
183
+ }
184
+ return matches;
185
+ }
186
+ /**
187
+ * Resalta las coincidencias en la capa de texto de una página. Restaura antes
188
+ * el texto original. Devuelve el resaltado de `selected`, si está en la página.
189
+ */
190
+ function highlightMatches(divs, strings, matches, selected) {
191
+ for (let i = 0; i < divs.length; i++) {
192
+ if (divs[i].childElementCount)
193
+ divs[i].textContent = strings[i] ?? '';
194
+ }
195
+ const segments = new Map();
196
+ for (const m of matches) {
197
+ for (let item = m.start.item; item <= m.end.item; item++) {
198
+ const from = item === m.start.item ? m.start.offset : 0;
199
+ const to = item === m.end.item ? m.end.offset : (strings[item]?.length ?? 0);
200
+ if (to <= from)
201
+ continue;
202
+ const list = segments.get(item) ?? [];
203
+ list.push({ from, to, selected: m === selected });
204
+ segments.set(item, list);
205
+ }
206
+ }
207
+ let selectedEl = null;
208
+ for (const [item, list] of segments) {
209
+ const div = divs[item];
210
+ const str = strings[item];
211
+ if (!div || str === undefined)
212
+ continue;
213
+ const doc = div.ownerDocument;
214
+ const nodes = [];
215
+ let pos = 0;
216
+ for (const s of list.sort((a, b) => a.from - b.from)) {
217
+ if (s.from > pos)
218
+ nodes.push(doc.createTextNode(str.slice(pos, s.from)));
219
+ const mark = doc.createElement('span');
220
+ mark.className = s.selected ? 'cb-pdf__hl cb-is-selected' : 'cb-pdf__hl';
221
+ mark.textContent = str.slice(Math.max(pos, s.from), s.to);
222
+ nodes.push(mark);
223
+ if (s.selected)
224
+ selectedEl ??= mark;
225
+ pos = Math.max(pos, s.to);
226
+ }
227
+ if (pos < str.length)
228
+ nodes.push(doc.createTextNode(str.slice(pos)));
229
+ div.replaceChildren(...nodes);
230
+ }
231
+ return selectedEl;
232
+ }
233
+ function normalizeChar(char) {
234
+ return char.normalize('NFKD').replace(/\p{M}/gu, '').toLocaleLowerCase();
235
+ }
236
+
237
+ let nextUid = 0;
238
+ /** Puntos PDF (1/72") a píxeles CSS (1/96"): 100 % es el tamaño real. */
239
+ const CSS_UNITS = 96 / 72;
240
+ const ZOOM_LEVELS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
241
+ const MIN_ZOOM = 0.25;
242
+ const MAX_ZOOM = 5;
243
+ /** Margen alrededor de las páginas (px), igual que en el CSS. */
244
+ const PAGE_GAP = 16;
245
+ /** Píxeles máximos de un canvas (evita agotar la memoria en zoom alto). */
246
+ const MAX_CANVAS_PIXELS = 2 ** 24;
247
+ const THUMB_WIDTH = 112;
248
+ const PDF_ICONS = {
249
+ 'zoom-in': zoomIn,
250
+ 'zoom-out': zoomOut,
251
+ printer,
252
+ 'panel-left': panelLeft,
253
+ };
254
+ /**
255
+ * Visor de PDF sobre [pdf.js](https://mozilla.github.io/pdf.js/) (`pdfjs-dist`,
256
+ * dependencia opcional que se carga al abrir el primer documento).
257
+ *
258
+ * Renderiza las páginas a medida que se ven, con texto seleccionable y
259
+ * legible por lectores de pantalla, búsqueda (ignora tildes y mayúsculas),
260
+ * miniaturas, zoom, giro, descarga, impresión y documentos con contraseña.
261
+ *
262
+ * @example
263
+ * ```html
264
+ * <cb-pdf-viewer src="/docs/extracto.pdf" [(page)]="pagina" zoom="page-width" />
265
+ * ```
266
+ */
267
+ class CbPdfViewer {
268
+ constructor() {
269
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
270
+ this.t = injectCbTranslation();
271
+ this.format = cbFormat;
272
+ /** Documento: URL, `ArrayBuffer`, `Uint8Array`, `Blob` o `File`. */
273
+ this.src = input(undefined, /* @ts-ignore */
274
+ ...(ngDevMode ? [{ debugName: "src" }] : /* istanbul ignore next */ []));
275
+ /** Nombre del archivo al descargar (por defecto, el de la URL o del `File`). */
276
+ this.fileName = input(undefined, /* @ts-ignore */
277
+ ...(ngDevMode ? [{ debugName: "fileName" }] : /* istanbul ignore next */ []));
278
+ /** Contraseña para abrir el documento sin pedirla. */
279
+ this.password = input(undefined, /* @ts-ignore */
280
+ ...(ngDevMode ? [{ debugName: "password" }] : /* istanbul ignore next */ []));
281
+ /** Envía cookies en peticiones a otro origen. */
282
+ this.withCredentials = input(false, { ...(ngDevMode ? { debugName: "withCredentials" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
283
+ /** Cabeceras HTTP para descargar el documento (p. ej. `Authorization`). */
284
+ this.httpHeaders = input(undefined, /* @ts-ignore */
285
+ ...(ngDevMode ? [{ debugName: "httpHeaders" }] : /* istanbul ignore next */ []));
286
+ /** Página actual (desde 1). */
287
+ this.page = model(1, /* @ts-ignore */
288
+ ...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
289
+ this.zoom = model('page-width', /* @ts-ignore */
290
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
291
+ this.rotation = model(0, /* @ts-ignore */
292
+ ...(ngDevMode ? [{ debugName: "rotation" }] : /* istanbul ignore next */ []));
293
+ /** Panel de miniaturas abierto. */
294
+ this.sidebar = model(false, /* @ts-ignore */
295
+ ...(ngDevMode ? [{ debugName: "sidebar" }] : /* istanbul ignore next */ []));
296
+ this.showToolbar = input(true, { ...(ngDevMode ? { debugName: "showToolbar" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
297
+ this.showSearch = input(true, { ...(ngDevMode ? { debugName: "showSearch" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
298
+ this.showThumbnails = input(true, { ...(ngDevMode ? { debugName: "showThumbnails" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
299
+ this.showRotate = input(true, { ...(ngDevMode ? { debugName: "showRotate" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
300
+ this.downloadable = input(true, { ...(ngDevMode ? { debugName: "downloadable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
301
+ this.printable = input(true, { ...(ngDevMode ? { debugName: "printable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
302
+ /** Nombre accesible del visor (por defecto, "Visor de PDF"). */
303
+ this.ariaLabel = input('', /* @ts-ignore */
304
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
305
+ /** Documento abierto. */
306
+ this.loaded = output();
307
+ /** No se pudo abrir el documento. */
308
+ this.loadError = output();
309
+ this.engine = inject(CbPdfEngine);
310
+ this.document = inject(DOCUMENT);
311
+ this.injector = inject(Injector);
312
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
313
+ this.uid = `cb-pdf-${++nextUid}`;
314
+ this.state = signal('idle', /* @ts-ignore */
315
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
316
+ this.progress = signal(null, /* @ts-ignore */
317
+ ...(ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []));
318
+ this.passwordIncorrect = signal(false, /* @ts-ignore */
319
+ ...(ngDevMode ? [{ debugName: "passwordIncorrect" }] : /* istanbul ignore next */ []));
320
+ this.printProgress = signal(null, /* @ts-ignore */
321
+ ...(ngDevMode ? [{ debugName: "printProgress" }] : /* istanbul ignore next */ []));
322
+ this.numPages = signal(0, /* @ts-ignore */
323
+ ...(ngDevMode ? [{ debugName: "numPages" }] : /* istanbul ignore next */ []));
324
+ this.sizes = signal([], /* @ts-ignore */
325
+ ...(ngDevMode ? [{ debugName: "sizes" }] : /* istanbul ignore next */ []));
326
+ this.box = signal({ width: 0, height: 0 }, /* @ts-ignore */
327
+ ...(ngDevMode ? [{ debugName: "box" }] : /* istanbul ignore next */ []));
328
+ this.query = signal('', /* @ts-ignore */
329
+ ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
330
+ this.matches = signal([], /* @ts-ignore */
331
+ ...(ngDevMode ? [{ debugName: "matches" }] : /* istanbul ignore next */ []));
332
+ this.matchIndex = signal(-1, /* @ts-ignore */
333
+ ...(ngDevMode ? [{ debugName: "matchIndex" }] : /* istanbul ignore next */ []));
334
+ this.searching = signal(false, /* @ts-ignore */
335
+ ...(ngDevMode ? [{ debugName: "searching" }] : /* istanbul ignore next */ []));
336
+ this.zoomLevels = ZOOM_LEVELS;
337
+ this.viewport = viewChild('viewport', /* @ts-ignore */
338
+ ...(ngDevMode ? [{ debugName: "viewport" }] : /* istanbul ignore next */ []));
339
+ this.pageEls = viewChildren('pageEl', /* @ts-ignore */
340
+ ...(ngDevMode ? [{ debugName: "pageEls" }] : /* istanbul ignore next */ []));
341
+ this.thumbEls = viewChildren('thumbEl', /* @ts-ignore */
342
+ ...(ngDevMode ? [{ debugName: "thumbEls" }] : /* istanbul ignore next */ []));
343
+ this.searchInput = viewChild('searchInput', /* @ts-ignore */
344
+ ...(ngDevMode ? [{ debugName: "searchInput" }] : /* istanbul ignore next */ []));
345
+ this.passwordInput = viewChild('passwordInput', /* @ts-ignore */
346
+ ...(ngDevMode ? [{ debugName: "passwordInput" }] : /* istanbul ignore next */ []));
347
+ /** Factor de zoom efectivo (1 = 100 %). */
348
+ this.scale = computed(() => {
349
+ const zoom = this.zoom();
350
+ if (typeof zoom === 'number')
351
+ return clamp(zoom, MIN_ZOOM, MAX_ZOOM);
352
+ const size = this.sizes()[this.page() - 1] ?? this.sizes()[0];
353
+ const { width, height } = this.box();
354
+ if (!size || width <= 0)
355
+ return 1;
356
+ const [w, h] = this.rotatedPoints(size);
357
+ const fitWidth = (width - PAGE_GAP * 2) / (w * CSS_UNITS);
358
+ const fit = zoom === 'page-fit' && height > 0 ? Math.min(fitWidth, (height - PAGE_GAP * 2) / (h * CSS_UNITS)) : fitWidth;
359
+ return clamp(fit, MIN_ZOOM, MAX_ZOOM);
360
+ }, /* @ts-ignore */
361
+ ...(ngDevMode ? [{ debugName: "scale" }] : /* istanbul ignore next */ []));
362
+ /** Tamaño de cada página en píxeles CSS con el zoom y el giro actuales. */
363
+ this.pageBoxes = computed(() => {
364
+ const factor = this.scale() * CSS_UNITS;
365
+ return this.sizes().map((size) => {
366
+ const [w, h] = this.rotatedPoints(size);
367
+ return { width: Math.floor(w * factor), height: Math.floor(h * factor), scaleFactor: factor, userUnit: size.userUnit };
368
+ });
369
+ }, /* @ts-ignore */
370
+ ...(ngDevMode ? [{ debugName: "pageBoxes" }] : /* istanbul ignore next */ []));
371
+ this.thumbBoxes = computed(() => this.sizes().map((size) => {
372
+ const [w, h] = this.rotatedPoints(size);
373
+ return { width: THUMB_WIDTH, height: Math.round((THUMB_WIDTH * h) / w) };
374
+ }), /* @ts-ignore */
375
+ ...(ngDevMode ? [{ debugName: "thumbBoxes" }] : /* istanbul ignore next */ []));
376
+ /** Valor del selector de zoom. */
377
+ this.zoomValue = computed(() => {
378
+ const zoom = this.zoom();
379
+ return typeof zoom === 'number' ? String(zoom) : zoom;
380
+ }, /* @ts-ignore */
381
+ ...(ngDevMode ? [{ debugName: "zoomValue" }] : /* istanbul ignore next */ []));
382
+ this.customZoom = computed(() => {
383
+ const zoom = this.zoom();
384
+ return typeof zoom === 'number' && !ZOOM_LEVELS.includes(zoom) ? zoom : null;
385
+ }, /* @ts-ignore */
386
+ ...(ngDevMode ? [{ debugName: "customZoom" }] : /* istanbul ignore next */ []));
387
+ this.searchStatus = computed(() => {
388
+ if (!this.query().trim())
389
+ return '';
390
+ const total = this.matches().length;
391
+ if (total)
392
+ return cbFormat(this.t().matchOf, this.matchIndex() + 1, total);
393
+ return this.searching() ? this.t().searching : this.t().noResults;
394
+ }, /* @ts-ignore */
395
+ ...(ngDevMode ? [{ debugName: "searchStatus" }] : /* istanbul ignore next */ []));
396
+ this.loadToken = 0;
397
+ this.searchToken = 0;
398
+ this.pages = new Map();
399
+ this.texts = new Map();
400
+ this.indexes = new Map();
401
+ this.rendered = new Map();
402
+ this.thumbs = new Map();
403
+ this.visible = new Set();
404
+ this.visibleThumbs = new Set();
405
+ /** Página que se ve al desplazarse (distingue el scroll del usuario de un cambio de `page`). */
406
+ this.visiblePage = 1;
407
+ this.scrollFrame = 0;
408
+ this.scrollToMatch = false;
409
+ const registry = inject(CbIconRegistry);
410
+ for (const [name, body] of Object.entries(PDF_ICONS)) {
411
+ if (!registry.has(name))
412
+ registry.register({ [name]: body });
413
+ }
414
+ effect(() => {
415
+ const src = this.src();
416
+ const password = this.password();
417
+ untracked(() => void this.open(src, password));
418
+ });
419
+ // Al cambiar el zoom o el giro se conserva el punto de lectura.
420
+ effect(() => {
421
+ this.scale();
422
+ this.rotation();
423
+ if (untracked(this.state) !== 'ready')
424
+ return;
425
+ const anchor = this.readAnchor();
426
+ afterNextRender(() => {
427
+ if (anchor)
428
+ this.restoreAnchor(anchor);
429
+ this.renderVisible();
430
+ this.renderThumbs();
431
+ }, { injector: this.injector });
432
+ });
433
+ // Cambio de `page` desde fuera (o desde la barra): desplaza hasta ella.
434
+ effect(() => {
435
+ const page = this.page();
436
+ if (this.state() !== 'ready' || page === this.visiblePage)
437
+ return;
438
+ afterNextRender(() => this.scrollToPage(page), { injector: this.injector });
439
+ });
440
+ effect(() => {
441
+ this.pageEls();
442
+ if (this.state() === 'ready')
443
+ untracked(() => this.observePages());
444
+ });
445
+ effect(() => {
446
+ this.thumbEls();
447
+ if (this.sidebar() && this.state() === 'ready')
448
+ untracked(() => this.observeThumbs());
449
+ });
450
+ effect(() => {
451
+ this.matches();
452
+ this.matchIndex();
453
+ untracked(() => this.refreshHighlights());
454
+ });
455
+ inject(DestroyRef).onDestroy(() => {
456
+ this.loadToken++;
457
+ this.close();
458
+ this.resizeObserver?.disconnect();
459
+ clearTimeout(this.searchTimer);
460
+ if (this.scrollFrame)
461
+ cancelAnimationFrame(this.scrollFrame);
462
+ });
463
+ }
464
+ // --- API pública -----------------------------------------------------------
465
+ goToPage(page) {
466
+ const n = clamp(Math.round(page), 1, Math.max(1, this.numPages()));
467
+ this.page.set(n);
468
+ if (n === this.visiblePage)
469
+ this.scrollToPage(n);
470
+ }
471
+ previousPage() {
472
+ this.goToPage(this.page() - 1);
473
+ }
474
+ nextPage() {
475
+ this.goToPage(this.page() + 1);
476
+ }
477
+ zoomIn() {
478
+ const scale = this.scale();
479
+ this.zoom.set(ZOOM_LEVELS.find((z) => z > scale + 0.01) ?? Math.min(MAX_ZOOM, round2(scale * 1.25)));
480
+ }
481
+ zoomOut() {
482
+ const scale = this.scale();
483
+ this.zoom.set([...ZOOM_LEVELS].reverse().find((z) => z < scale - 0.01) ?? Math.max(MIN_ZOOM, round2(scale / 1.25)));
484
+ }
485
+ /** Gira 90° a la derecha (o a la izquierda con `-1`). */
486
+ rotate(direction = 1) {
487
+ this.rotation.set((((this.rotation() + direction * 90) % 360) + 360) % 360);
488
+ }
489
+ /** Busca en todo el documento; resalta y lleva a la primera coincidencia. */
490
+ async search(query) {
491
+ clearTimeout(this.searchTimer);
492
+ this.query.set(query);
493
+ const token = ++this.searchToken;
494
+ this.matches.set([]);
495
+ this.matchIndex.set(-1);
496
+ if (!query.trim() || !this.pdf) {
497
+ this.searching.set(false);
498
+ return;
499
+ }
500
+ this.searching.set(true);
501
+ const found = [];
502
+ // La primera coincidencia se busca desde la página actual.
503
+ const start = this.page();
504
+ for (let n = 1; n <= this.numPages(); n++) {
505
+ const index = await this.textIndex(n).catch(() => null);
506
+ if (token !== this.searchToken)
507
+ return;
508
+ if (!index)
509
+ continue;
510
+ const pageMatches = findMatches(index, query, n);
511
+ if (!pageMatches.length)
512
+ continue;
513
+ found.push(...pageMatches);
514
+ this.matches.set([...found]);
515
+ const current = this.matchIndex();
516
+ const first = found.findIndex((m) => m.page >= start);
517
+ if (current === -1 && first !== -1)
518
+ this.selectMatch(first);
519
+ }
520
+ if (token !== this.searchToken)
521
+ return;
522
+ this.searching.set(false);
523
+ if (this.matchIndex() === -1 && found.length)
524
+ this.selectMatch(0);
525
+ }
526
+ findNext() {
527
+ const total = this.matches().length;
528
+ if (total)
529
+ this.selectMatch((this.matchIndex() + 1) % total);
530
+ }
531
+ findPrevious() {
532
+ const total = this.matches().length;
533
+ if (total)
534
+ this.selectMatch((this.matchIndex() - 1 + total) % total);
535
+ }
536
+ /** Descarga el documento original. */
537
+ async download() {
538
+ if (!this.pdf)
539
+ return;
540
+ const src = this.src();
541
+ const blob = src instanceof Blob ? src : new Blob([(await this.pdf.getData())], { type: 'application/pdf' });
542
+ const url = URL.createObjectURL(blob);
543
+ const a = this.document.createElement('a');
544
+ a.href = url;
545
+ a.download = this.downloadName();
546
+ a.style.display = 'none';
547
+ this.document.body.append(a);
548
+ a.click();
549
+ a.remove();
550
+ setTimeout(() => URL.revokeObjectURL(url), 0);
551
+ }
552
+ /** Imprime el documento (no la página de la aplicación). */
553
+ async print() {
554
+ if (!this.pdf || this.printProgress())
555
+ return;
556
+ this.printProgress.set({ done: 0, total: this.numPages() });
557
+ try {
558
+ await printPdfDocument(this.pdf, this.document, (done, total) => this.printProgress.set({ done, total }), this.rotation());
559
+ }
560
+ finally {
561
+ this.printProgress.set(null);
562
+ }
563
+ }
564
+ // --- Carga -----------------------------------------------------------------
565
+ async open(src, password) {
566
+ const token = ++this.loadToken;
567
+ this.close();
568
+ if (!src || !this.isBrowser) {
569
+ this.state.set('idle');
570
+ return;
571
+ }
572
+ this.state.set('loading');
573
+ this.progress.set(null);
574
+ this.passwordIncorrect.set(false);
575
+ try {
576
+ const pdfjs = await this.engine.load();
577
+ const data = await toData(src);
578
+ if (token !== this.loadToken)
579
+ return;
580
+ this.pdfjs = pdfjs;
581
+ const task = pdfjs.getDocument({
582
+ ...this.engine.resources(),
583
+ ...(data ? { data } : { url: this.engine.resolve(src) }),
584
+ password,
585
+ withCredentials: this.withCredentials(),
586
+ httpHeaders: this.httpHeaders(),
587
+ });
588
+ this.loadingTask = task;
589
+ task.onProgress = ({ loaded, total }) => {
590
+ if (token === this.loadToken)
591
+ this.progress.set(total ? Math.min(100, Math.round((loaded / total) * 100)) : null);
592
+ };
593
+ task.onPassword = (update, reason) => {
594
+ if (token !== this.loadToken)
595
+ return;
596
+ this.passwordCallback = update;
597
+ this.passwordIncorrect.set(reason === (pdfjs.PasswordResponses?.INCORRECT_PASSWORD ?? 2));
598
+ this.state.set('password');
599
+ afterNextRender(() => this.passwordInput()?.nativeElement.focus(), { injector: this.injector });
600
+ };
601
+ const pdf = await task.promise;
602
+ if (token !== this.loadToken) {
603
+ void task.destroy().catch(() => undefined);
604
+ return;
605
+ }
606
+ this.pdf = pdf;
607
+ // Mide el área antes del primer render para que "ajustar al ancho" no renderice dos veces.
608
+ this.observeResize();
609
+ const first = sizeOf(await this.getPage(1));
610
+ if (token !== this.loadToken)
611
+ return;
612
+ this.sizes.set(Array.from({ length: pdf.numPages }, () => first));
613
+ this.numPages.set(pdf.numPages);
614
+ const page = clamp(this.page(), 1, pdf.numPages);
615
+ this.visiblePage = 1;
616
+ this.page.set(page);
617
+ this.state.set('ready');
618
+ this.loaded.emit({ numPages: pdf.numPages });
619
+ afterNextRender(() => {
620
+ if (page > 1)
621
+ this.scrollToPage(page);
622
+ this.renderVisible();
623
+ }, { injector: this.injector });
624
+ void this.measurePages(token);
625
+ }
626
+ catch (error) {
627
+ if (token !== this.loadToken)
628
+ return;
629
+ this.state.set('error');
630
+ if (isDevMode())
631
+ console.error('[cb-pdf-viewer]', error);
632
+ this.loadError.emit(error);
633
+ }
634
+ }
635
+ submitPassword(event, value) {
636
+ event.preventDefault();
637
+ if (!this.passwordCallback)
638
+ return;
639
+ this.state.set('loading');
640
+ this.passwordCallback(value);
641
+ }
642
+ /** Lee el tamaño real de todas las páginas (pueden ser distintas de la primera). */
643
+ async measurePages(token) {
644
+ const sizes = [...this.sizes()];
645
+ let changed = false;
646
+ for (let n = 2; n <= sizes.length; n++) {
647
+ const page = await this.getPage(n).catch(() => null);
648
+ if (token !== this.loadToken)
649
+ return;
650
+ if (!page)
651
+ continue;
652
+ const size = sizeOf(page);
653
+ const prev = sizes[n - 1];
654
+ if (size.width !== prev.width || size.height !== prev.height || size.rotate !== prev.rotate || size.userUnit !== prev.userUnit) {
655
+ sizes[n - 1] = size;
656
+ changed = true;
657
+ }
658
+ }
659
+ if (changed)
660
+ this.sizes.set(sizes);
661
+ }
662
+ close() {
663
+ for (const page of [...this.rendered.keys()])
664
+ this.unrender(page);
665
+ this.pageObserver?.disconnect();
666
+ this.thumbObserver?.disconnect();
667
+ this.pageObserver = this.thumbObserver = undefined;
668
+ void this.loadingTask?.destroy().catch(() => undefined);
669
+ this.loadingTask = undefined;
670
+ this.pdf = undefined;
671
+ this.passwordCallback = undefined;
672
+ this.pages.clear();
673
+ this.texts.clear();
674
+ this.indexes.clear();
675
+ this.thumbs.clear();
676
+ this.visible.clear();
677
+ this.visibleThumbs.clear();
678
+ this.searchToken++;
679
+ this.matches.set([]);
680
+ this.matchIndex.set(-1);
681
+ this.searching.set(false);
682
+ this.numPages.set(0);
683
+ this.sizes.set([]);
684
+ }
685
+ getPage(n) {
686
+ let page = this.pages.get(n);
687
+ if (!page) {
688
+ page = this.pdf.getPage(n);
689
+ this.pages.set(n, page);
690
+ }
691
+ return page;
692
+ }
693
+ textContent(n) {
694
+ let text = this.texts.get(n);
695
+ if (!text) {
696
+ text = this.getPage(n).then((page) => page.getTextContent());
697
+ this.texts.set(n, text);
698
+ }
699
+ return text;
700
+ }
701
+ textIndex(n) {
702
+ let index = this.indexes.get(n);
703
+ if (!index) {
704
+ index = this.textContent(n).then((text) => buildTextIndex(text.items));
705
+ this.indexes.set(n, index);
706
+ }
707
+ return index;
708
+ }
709
+ // --- Renderizado ------------------------------------------------------------
710
+ /** Observa qué páginas están cerca de la vista para renderizar solo esas. */
711
+ observePages() {
712
+ const root = this.viewport()?.nativeElement;
713
+ if (!root)
714
+ return;
715
+ this.pageObserver?.disconnect();
716
+ if (typeof IntersectionObserver === 'undefined') {
717
+ this.visible.clear();
718
+ for (let n = 1; n <= this.numPages(); n++)
719
+ this.visible.add(n);
720
+ this.renderVisible();
721
+ return;
722
+ }
723
+ this.pageObserver = new IntersectionObserver((entries) => {
724
+ for (const entry of entries) {
725
+ const n = Number(entry.target.dataset['page']);
726
+ if (entry.isIntersecting)
727
+ this.visible.add(n);
728
+ else {
729
+ this.visible.delete(n);
730
+ this.unrender(n);
731
+ }
732
+ }
733
+ this.renderVisible();
734
+ }, { root, rootMargin: '50% 0px' });
735
+ for (const el of this.pageEls())
736
+ this.pageObserver.observe(el.nativeElement);
737
+ }
738
+ renderVisible() {
739
+ const current = this.page();
740
+ const pages = [...this.visible].sort((a, b) => Math.abs(a - current) - Math.abs(b - current));
741
+ for (const n of pages)
742
+ void this.renderPage(n);
743
+ }
744
+ async renderPage(n) {
745
+ const scale = this.scale() * CSS_UNITS;
746
+ const rotation = this.rotation();
747
+ const key = `${scale}:${rotation}`;
748
+ if (this.rendered.get(n)?.key === key || !this.pdf)
749
+ return;
750
+ this.unrender(n);
751
+ let cancelled = false;
752
+ let renderTask;
753
+ const entry = {
754
+ key,
755
+ cancel: () => {
756
+ cancelled = true;
757
+ renderTask?.cancel();
758
+ entry.textLayer?.cancel();
759
+ },
760
+ };
761
+ this.rendered.set(n, entry);
762
+ try {
763
+ const page = await this.getPage(n);
764
+ const el = this.pageEl(n);
765
+ if (cancelled || !el)
766
+ return;
767
+ const viewport = page.getViewport({ scale, rotation: (page.rotate + rotation) % 360 });
768
+ const canvas = this.document.createElement('canvas');
769
+ const ratio = outputScale(viewport.width, viewport.height);
770
+ canvas.width = Math.floor(viewport.width * ratio);
771
+ canvas.height = Math.floor(viewport.height * ratio);
772
+ canvas.setAttribute('aria-hidden', 'true');
773
+ const task = page.render({ canvas, viewport, transform: ratio === 1 ? undefined : [ratio, 0, 0, ratio, 0, 0] });
774
+ renderTask = task;
775
+ await task.promise;
776
+ if (cancelled)
777
+ return;
778
+ el.querySelector('.cb-pdf__canvas')?.replaceChildren(canvas);
779
+ const textEl = el.querySelector('.cb-pdf__text');
780
+ if (!textEl || !this.pdfjs?.TextLayer)
781
+ return;
782
+ const textContentSource = await this.textContent(n);
783
+ if (cancelled)
784
+ return;
785
+ textEl.replaceChildren();
786
+ const textLayer = new this.pdfjs.TextLayer({ textContentSource, container: textEl, viewport });
787
+ entry.textLayer = textLayer;
788
+ await textLayer.render();
789
+ if (cancelled)
790
+ return;
791
+ this.highlightPage(n, entry);
792
+ }
793
+ catch {
794
+ // Render cancelado o página dañada: se deja en blanco.
795
+ if (this.rendered.get(n) === entry && !cancelled)
796
+ entry.key = '';
797
+ }
798
+ }
799
+ unrender(n) {
800
+ const entry = this.rendered.get(n);
801
+ if (!entry)
802
+ return;
803
+ entry.cancel();
804
+ this.rendered.delete(n);
805
+ const el = this.pageEl(n);
806
+ el?.querySelector('.cb-pdf__canvas')?.replaceChildren();
807
+ el?.querySelector('.cb-pdf__text')?.replaceChildren();
808
+ }
809
+ observeThumbs() {
810
+ const root = this.document.getElementById(`${this.uid}-thumbs`);
811
+ if (!root)
812
+ return;
813
+ this.thumbObserver?.disconnect();
814
+ if (typeof IntersectionObserver === 'undefined') {
815
+ for (let n = 1; n <= this.numPages(); n++)
816
+ this.visibleThumbs.add(n);
817
+ this.renderThumbs();
818
+ return;
819
+ }
820
+ this.thumbObserver = new IntersectionObserver((entries) => {
821
+ for (const entry of entries) {
822
+ const n = Number(entry.target.dataset['page']);
823
+ if (entry.isIntersecting)
824
+ this.visibleThumbs.add(n);
825
+ else
826
+ this.visibleThumbs.delete(n);
827
+ }
828
+ this.renderThumbs();
829
+ }, { root, rootMargin: '100% 0px' });
830
+ for (const el of this.thumbEls())
831
+ this.thumbObserver.observe(el.nativeElement);
832
+ }
833
+ renderThumbs() {
834
+ if (!this.sidebar())
835
+ return;
836
+ for (const n of this.visibleThumbs)
837
+ void this.renderThumb(n);
838
+ }
839
+ async renderThumb(n) {
840
+ const key = String(this.rotation());
841
+ if (this.thumbs.get(n) === key || !this.pdf)
842
+ return;
843
+ this.thumbs.set(n, key);
844
+ try {
845
+ const page = await this.getPage(n);
846
+ const rotation = (page.rotate + this.rotation()) % 360;
847
+ const base = page.getViewport({ scale: 1, rotation });
848
+ const ratio = Math.min(2, globalThis.devicePixelRatio || 1);
849
+ const viewport = page.getViewport({ scale: (THUMB_WIDTH / base.width) * ratio, rotation });
850
+ const canvas = this.document.createElement('canvas');
851
+ canvas.width = Math.floor(viewport.width);
852
+ canvas.height = Math.floor(viewport.height);
853
+ canvas.setAttribute('aria-hidden', 'true');
854
+ await page.render({ canvas, viewport }).promise;
855
+ if (this.thumbs.get(n) !== key)
856
+ return;
857
+ this.thumbEls()
858
+ .find((el) => el.nativeElement.dataset['page'] === String(n))
859
+ ?.nativeElement.querySelector('.cb-pdf__thumb-canvas')
860
+ ?.replaceChildren(canvas);
861
+ }
862
+ catch {
863
+ this.thumbs.delete(n);
864
+ }
865
+ }
866
+ // --- Búsqueda ---------------------------------------------------------------
867
+ selectMatch(index) {
868
+ this.matchIndex.set(index);
869
+ const match = this.matches()[index];
870
+ if (!match)
871
+ return;
872
+ this.scrollToMatch = true;
873
+ if (this.rendered.get(match.page)?.textLayer)
874
+ this.refreshHighlights();
875
+ else
876
+ this.goToPage(match.page);
877
+ }
878
+ refreshHighlights() {
879
+ for (const [n, entry] of this.rendered)
880
+ this.highlightPage(n, entry);
881
+ }
882
+ highlightPage(n, entry) {
883
+ const layer = entry.textLayer;
884
+ if (!layer)
885
+ return;
886
+ const matches = this.matches();
887
+ const selected = matches[this.matchIndex()] ?? null;
888
+ const el = highlightMatches(layer.textDivs, layer.textContentItemsStr, matches.filter((m) => m.page === n), selected?.page === n ? selected : null);
889
+ if (el && this.scrollToMatch) {
890
+ this.scrollToMatch = false;
891
+ el.scrollIntoView?.({ block: 'center', inline: 'nearest' });
892
+ }
893
+ }
894
+ onSearchInput(value) {
895
+ this.query.set(value);
896
+ clearTimeout(this.searchTimer);
897
+ this.searchTimer = setTimeout(() => void this.search(value), 250);
898
+ }
899
+ onSearchKeydown(event, value) {
900
+ if (event.key === 'Enter') {
901
+ event.preventDefault();
902
+ if (value !== this.query() || (!this.matches().length && !this.searching()))
903
+ void this.search(value);
904
+ else if (event.shiftKey)
905
+ this.findPrevious();
906
+ else
907
+ this.findNext();
908
+ }
909
+ else if (event.key === 'Escape' && value) {
910
+ event.preventDefault();
911
+ event.stopPropagation();
912
+ event.target.value = '';
913
+ void this.search('');
914
+ }
915
+ }
916
+ // --- Desplazamiento y tamaño ---------------------------------------------------
917
+ onScroll() {
918
+ cancelAnimationFrame(this.scrollFrame);
919
+ this.scrollFrame = requestAnimationFrame(() => {
920
+ const page = this.mostVisiblePage();
921
+ if (page && page !== this.visiblePage) {
922
+ this.visiblePage = page;
923
+ this.page.set(page);
924
+ }
925
+ });
926
+ }
927
+ mostVisiblePage() {
928
+ const root = this.viewport()?.nativeElement;
929
+ if (!root)
930
+ return null;
931
+ const top = root.scrollTop;
932
+ const bottom = top + root.clientHeight;
933
+ let best = null;
934
+ let bestArea = 0;
935
+ for (const { nativeElement: el } of this.pageEls()) {
936
+ const visible = Math.min(bottom, el.offsetTop + el.offsetHeight) - Math.max(top, el.offsetTop);
937
+ if (visible > bestArea) {
938
+ bestArea = visible;
939
+ best = Number(el.dataset['page']);
940
+ }
941
+ }
942
+ return best;
943
+ }
944
+ scrollToPage(n) {
945
+ const root = this.viewport()?.nativeElement;
946
+ const el = this.pageEl(n);
947
+ if (!root || !el)
948
+ return;
949
+ this.visiblePage = n;
950
+ root.scrollTop = el.offsetTop - PAGE_GAP;
951
+ }
952
+ readAnchor() {
953
+ const root = this.viewport()?.nativeElement;
954
+ const el = this.pageEl(this.visiblePage);
955
+ if (!root || !el || !el.offsetHeight)
956
+ return null;
957
+ return { page: this.visiblePage, ratio: (root.scrollTop - el.offsetTop) / el.offsetHeight };
958
+ }
959
+ restoreAnchor(anchor) {
960
+ const root = this.viewport()?.nativeElement;
961
+ const el = this.pageEl(anchor.page);
962
+ if (root && el)
963
+ root.scrollTop = el.offsetTop + anchor.ratio * el.offsetHeight;
964
+ }
965
+ observeResize() {
966
+ const root = this.viewport()?.nativeElement;
967
+ if (!root)
968
+ return;
969
+ const measure = () => this.box.set({ width: root.clientWidth, height: root.clientHeight });
970
+ measure();
971
+ if (typeof ResizeObserver === 'undefined' || this.resizeObserver)
972
+ return;
973
+ this.resizeObserver = new ResizeObserver(measure);
974
+ this.resizeObserver.observe(root);
975
+ }
976
+ pageEl(n) {
977
+ return this.pageEls()[n - 1]?.nativeElement;
978
+ }
979
+ // --- Barra y teclado -------------------------------------------------------------
980
+ onPageInput(event) {
981
+ const input = event.target;
982
+ const value = Number.parseInt(input.value, 10);
983
+ if (Number.isFinite(value))
984
+ this.goToPage(value);
985
+ input.value = String(this.page());
986
+ }
987
+ onZoomSelect(value) {
988
+ this.zoom.set(value === 'page-width' || value === 'page-fit' ? value : Number(value));
989
+ }
990
+ onThumbKeydown(event, n) {
991
+ const next = event.key === 'ArrowDown' ? n + 1 : event.key === 'ArrowUp' ? n - 1 : event.key === 'Home' ? 1 : event.key === 'End' ? this.numPages() : 0;
992
+ if (!next || next < 1 || next > this.numPages())
993
+ return;
994
+ event.preventDefault();
995
+ this.thumbEls()[next - 1]?.nativeElement.focus();
996
+ }
997
+ onHostKeydown(event) {
998
+ if (this.state() !== 'ready' || !(event.ctrlKey || event.metaKey) || event.altKey)
999
+ return;
1000
+ const key = event.key.toLowerCase();
1001
+ if (key === '+' || key === '=')
1002
+ this.zoomIn();
1003
+ else if (key === '-')
1004
+ this.zoomOut();
1005
+ else if (key === '0')
1006
+ this.zoom.set('page-width');
1007
+ else if (key === 'f' && this.showToolbar() && this.showSearch()) {
1008
+ const input = this.searchInput()?.nativeElement;
1009
+ input?.focus();
1010
+ input?.select();
1011
+ }
1012
+ else if (key === 'p' && this.printable())
1013
+ void this.print();
1014
+ else
1015
+ return;
1016
+ event.preventDefault();
1017
+ }
1018
+ onWheel(event) {
1019
+ if (!(event.ctrlKey || event.metaKey) || this.state() !== 'ready')
1020
+ return;
1021
+ event.preventDefault();
1022
+ if (event.deltaY < 0)
1023
+ this.zoomIn();
1024
+ else
1025
+ this.zoomOut();
1026
+ }
1027
+ percent(zoom) {
1028
+ return `${Math.round(zoom * 100)} %`;
1029
+ }
1030
+ downloadName() {
1031
+ const name = this.fileName();
1032
+ if (name)
1033
+ return name;
1034
+ const src = this.src();
1035
+ if (typeof File !== 'undefined' && src instanceof File && src.name)
1036
+ return src.name;
1037
+ if (typeof src === 'string' || src instanceof URL) {
1038
+ const last = decodeURIComponent(String(src).split(/[?#]/)[0].split('/').pop() ?? '');
1039
+ if (last)
1040
+ return /\.pdf$/i.test(last) ? last : `${last}.pdf`;
1041
+ }
1042
+ return 'document.pdf';
1043
+ }
1044
+ rotatedPoints(size) {
1045
+ return (size.rotate + this.rotation()) % 180 === 0 ? [size.width, size.height] : [size.height, size.width];
1046
+ }
1047
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbPdfViewer, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1048
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbPdfViewer, isStandalone: true, selector: "cb-pdf-viewer", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, fileName: { classPropertyName: "fileName", publicName: "fileName", isSignal: true, isRequired: false, transformFunction: null }, password: { classPropertyName: "password", publicName: "password", isSignal: true, isRequired: false, transformFunction: null }, withCredentials: { classPropertyName: "withCredentials", publicName: "withCredentials", isSignal: true, isRequired: false, transformFunction: null }, httpHeaders: { classPropertyName: "httpHeaders", publicName: "httpHeaders", isSignal: true, isRequired: false, transformFunction: null }, page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, rotation: { classPropertyName: "rotation", publicName: "rotation", isSignal: true, isRequired: false, transformFunction: null }, sidebar: { classPropertyName: "sidebar", publicName: "sidebar", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, showThumbnails: { classPropertyName: "showThumbnails", publicName: "showThumbnails", isSignal: true, isRequired: false, transformFunction: null }, showRotate: { classPropertyName: "showRotate", publicName: "showRotate", isSignal: true, isRequired: false, transformFunction: null }, downloadable: { classPropertyName: "downloadable", publicName: "downloadable", isSignal: true, isRequired: false, transformFunction: null }, printable: { classPropertyName: "printable", publicName: "printable", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { page: "pageChange", zoom: "zoomChange", rotation: "rotationChange", sidebar: "sidebarChange", loaded: "loaded", loadError: "loadError" }, host: { attributes: { "role": "region" }, listeners: { "keydown": "onHostKeydown($event)" }, properties: { "attr.aria-label": "ariaLabel() || t().pdfViewer" }, classAttribute: "cb-pdf-viewer" }, viewQueries: [{ propertyName: "viewport", first: true, predicate: ["viewport"], descendants: true, isSignal: true }, { propertyName: "pageEls", predicate: ["pageEl"], descendants: true, isSignal: true }, { propertyName: "thumbEls", predicate: ["thumbEl"], descendants: true, isSignal: true }, { propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "passwordInput", first: true, predicate: ["passwordInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
1049
+ @if (showToolbar()) {
1050
+ <div class="cb-pdf__toolbar">
1051
+ @if (showThumbnails()) {
1052
+ <div class="cb-pdf__group">
1053
+ <button
1054
+ type="button"
1055
+ class="cb-pdf__tool"
1056
+ [attr.aria-label]="t().thumbnails"
1057
+ [attr.aria-expanded]="sidebar()"
1058
+ [attr.aria-controls]="uid + '-thumbs'"
1059
+ [disabled]="state() !== 'ready'"
1060
+ (click)="sidebar.set(!sidebar())"
1061
+ >
1062
+ <cb-icon name="panel-left" [size]="18" aria-hidden="true" />
1063
+ </button>
1064
+ </div>
1065
+ }
1066
+
1067
+ <div class="cb-pdf__group" role="group" [attr.aria-label]="t().pagination">
1068
+ <button
1069
+ type="button"
1070
+ class="cb-pdf__tool"
1071
+ [attr.aria-label]="t().previousPage"
1072
+ [disabled]="state() !== 'ready' || page() <= 1"
1073
+ (click)="previousPage()"
1074
+ >
1075
+ <cb-icon name="chevron-up" [size]="18" aria-hidden="true" />
1076
+ </button>
1077
+ <button
1078
+ type="button"
1079
+ class="cb-pdf__tool"
1080
+ [attr.aria-label]="t().nextPage"
1081
+ [disabled]="state() !== 'ready' || page() >= numPages()"
1082
+ (click)="nextPage()"
1083
+ >
1084
+ <cb-icon name="chevron-down" [size]="18" aria-hidden="true" />
1085
+ </button>
1086
+ <input
1087
+ class="cb-pdf__field cb-pdf__page-input"
1088
+ type="text"
1089
+ inputmode="numeric"
1090
+ autocomplete="off"
1091
+ [id]="uid + '-page'"
1092
+ [attr.aria-label]="t().pageNumber"
1093
+ [attr.aria-describedby]="uid + '-total'"
1094
+ [value]="page()"
1095
+ [disabled]="state() !== 'ready'"
1096
+ (change)="onPageInput($event)"
1097
+ (keydown.enter)="onPageInput($event)"
1098
+ />
1099
+ <span class="cb-pdf__total" [id]="uid + '-total'">{{ format(t().ofTotal, numPages() || '–') }}</span>
1100
+ </div>
1101
+
1102
+ <div class="cb-pdf__group" role="group" [attr.aria-label]="t().zoomLevel">
1103
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().zoomOut" [disabled]="state() !== 'ready' || scale() <= 0.25" (click)="zoomOut()">
1104
+ <cb-icon name="zoom-out" [size]="18" aria-hidden="true" />
1105
+ </button>
1106
+ <select
1107
+ class="cb-pdf__field cb-pdf__zoom"
1108
+ [attr.aria-label]="t().zoomLevel"
1109
+ [disabled]="state() !== 'ready'"
1110
+ (change)="onZoomSelect($any($event.target).value)"
1111
+ >
1112
+ <option value="page-width" [selected]="zoomValue() === 'page-width'">{{ t().fitWidth }}</option>
1113
+ <option value="page-fit" [selected]="zoomValue() === 'page-fit'">{{ t().fitPage }}</option>
1114
+ @if (customZoom(); as custom) {
1115
+ <option [value]="custom" selected>{{ percent(custom) }}</option>
1116
+ }
1117
+ @for (level of zoomLevels; track level) {
1118
+ <option [value]="level" [selected]="zoomValue() === '' + level">{{ percent(level) }}</option>
1119
+ }
1120
+ </select>
1121
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().zoomIn" [disabled]="state() !== 'ready' || scale() >= 5" (click)="zoomIn()">
1122
+ <cb-icon name="zoom-in" [size]="18" aria-hidden="true" />
1123
+ </button>
1124
+ @if (showRotate()) {
1125
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().rotateRight" [disabled]="state() !== 'ready'" (click)="rotate()">
1126
+ <cb-icon name="rotate-cw" [size]="18" aria-hidden="true" />
1127
+ </button>
1128
+ }
1129
+ </div>
1130
+
1131
+ <span class="cb-pdf__spacer"></span>
1132
+
1133
+ @if (showSearch()) {
1134
+ <div class="cb-pdf__group cb-pdf__search" role="search">
1135
+ <cb-icon class="cb-pdf__search-icon" name="search" [size]="16" aria-hidden="true" />
1136
+ <input
1137
+ #searchInput
1138
+ class="cb-pdf__field cb-pdf__search-input"
1139
+ type="search"
1140
+ autocomplete="off"
1141
+ [attr.aria-label]="t().searchInDocument"
1142
+ [attr.aria-describedby]="uid + '-status'"
1143
+ [placeholder]="t().searchInDocument"
1144
+ [disabled]="state() !== 'ready'"
1145
+ (input)="onSearchInput(searchInput.value)"
1146
+ (keydown)="onSearchKeydown($event, searchInput.value)"
1147
+ />
1148
+ <span class="cb-pdf__status" [id]="uid + '-status'" aria-live="polite">{{ searchStatus() }}</span>
1149
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().previousMatch" [disabled]="!matches().length" (click)="findPrevious()">
1150
+ <cb-icon name="chevron-left" [size]="18" aria-hidden="true" />
1151
+ </button>
1152
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().nextMatch" [disabled]="!matches().length" (click)="findNext()">
1153
+ <cb-icon name="chevron-right" [size]="18" aria-hidden="true" />
1154
+ </button>
1155
+ </div>
1156
+ }
1157
+
1158
+ @if (downloadable() || printable()) {
1159
+ <div class="cb-pdf__group">
1160
+ @if (downloadable()) {
1161
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().download" [disabled]="state() !== 'ready'" (click)="download()">
1162
+ <cb-icon name="download" [size]="18" aria-hidden="true" />
1163
+ </button>
1164
+ }
1165
+ @if (printable()) {
1166
+ <button
1167
+ type="button"
1168
+ class="cb-pdf__tool"
1169
+ [attr.aria-label]="t().print"
1170
+ [disabled]="state() !== 'ready' || !!printProgress()"
1171
+ (click)="print()"
1172
+ >
1173
+ <cb-icon name="printer" [size]="18" aria-hidden="true" />
1174
+ </button>
1175
+ }
1176
+ </div>
1177
+ }
1178
+ </div>
1179
+ }
1180
+
1181
+ <div class="cb-pdf__body">
1182
+ @if (state() === 'ready' && sidebar() && showThumbnails()) {
1183
+ <nav class="cb-pdf__sidebar" [id]="uid + '-thumbs'" [attr.aria-label]="t().thumbnails">
1184
+ @for (thumb of thumbBoxes(); track $index; let i = $index) {
1185
+ <button
1186
+ #thumbEl
1187
+ type="button"
1188
+ class="cb-pdf__thumb"
1189
+ [class.cb-is-active]="page() === i + 1"
1190
+ [attr.data-page]="i + 1"
1191
+ [attr.aria-label]="format(t().pageOf, i + 1, numPages())"
1192
+ [attr.aria-current]="page() === i + 1 ? 'page' : null"
1193
+ (click)="goToPage(i + 1)"
1194
+ (keydown)="onThumbKeydown($event, i + 1)"
1195
+ >
1196
+ <span class="cb-pdf__thumb-canvas" [style.width.px]="thumb.width" [style.height.px]="thumb.height"></span>
1197
+ <span class="cb-pdf__thumb-label" aria-hidden="true">{{ i + 1 }}</span>
1198
+ </button>
1199
+ }
1200
+ </nav>
1201
+ }
1202
+
1203
+ <div
1204
+ #viewport
1205
+ class="cb-pdf__viewport"
1206
+ tabindex="0"
1207
+ role="document"
1208
+ [attr.aria-label]="t().pdfDocument"
1209
+ [attr.aria-busy]="state() === 'loading' ? 'true' : null"
1210
+ (scroll)="onScroll()"
1211
+ (wheel)="onWheel($event)"
1212
+ >
1213
+ @switch (state()) {
1214
+ @case ('loading') {
1215
+ <div class="cb-pdf__message" role="status">
1216
+ <cb-spinner [size]="32" aria-hidden="true" />
1217
+ <span>{{ t().loadingDocument }}@if (progress() !== null) { {{ progress() }} %}</span>
1218
+ </div>
1219
+ }
1220
+ @case ('error') {
1221
+ <div class="cb-pdf__message cb-pdf__message--error" role="alert">{{ t().documentError }}</div>
1222
+ }
1223
+ @case ('password') {
1224
+ <form class="cb-pdf__password" (submit)="submitPassword($event, passwordInput.value)">
1225
+ <cb-icon name="lock" [size]="28" aria-hidden="true" />
1226
+ <p class="cb-pdf__password-text" [id]="uid + '-pwd-text'">{{ t().protectedDocument }}</p>
1227
+ <label class="cb-pdf__password-label" [for]="uid + '-pwd'">{{ t().password }}</label>
1228
+ <div class="cb-pdf__password-row">
1229
+ <input
1230
+ #passwordInput
1231
+ class="cb-pdf__field"
1232
+ type="password"
1233
+ autocomplete="off"
1234
+ [id]="uid + '-pwd'"
1235
+ [attr.aria-describedby]="passwordIncorrect() ? uid + '-pwd-error' : uid + '-pwd-text'"
1236
+ [attr.aria-invalid]="passwordIncorrect() ? 'true' : null"
1237
+ />
1238
+ <button type="submit" class="cb-pdf__submit">{{ t().open }}</button>
1239
+ </div>
1240
+ @if (passwordIncorrect()) {
1241
+ <p class="cb-pdf__password-error" [id]="uid + '-pwd-error'" role="alert">{{ t().incorrectPassword }}</p>
1242
+ }
1243
+ </form>
1244
+ }
1245
+ @case ('ready') {
1246
+ <div class="cb-pdf__pages">
1247
+ @for (box of pageBoxes(); track $index; let i = $index) {
1248
+ <div
1249
+ #pageEl
1250
+ class="cb-pdf__page"
1251
+ role="group"
1252
+ [attr.data-page]="i + 1"
1253
+ [attr.aria-label]="format(t().pageOf, i + 1, numPages())"
1254
+ [style.width.px]="box.width"
1255
+ [style.height.px]="box.height"
1256
+ [style.--scale-factor]="box.scaleFactor"
1257
+ [style.--user-unit]="box.userUnit"
1258
+ >
1259
+ <div class="cb-pdf__canvas"></div>
1260
+ <div class="cb-pdf__text"></div>
1261
+ </div>
1262
+ }
1263
+ </div>
1264
+ }
1265
+ }
1266
+ </div>
1267
+ </div>
1268
+
1269
+ @if (printProgress(); as p) {
1270
+ <div class="cb-pdf__toast" role="status">{{ format(t().preparingPrint, p.done, p.total) }}</div>
1271
+ }
1272
+ `, isInline: true, styles: [":host{position:relative;display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:var(--cb-pdf-viewer-height, 40rem);min-height:16rem;overflow:hidden;border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);color:var(--cb-color-text);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm);container-type:inline-size}.cb-pdf__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width, 1px) solid var(--cb-color-border);background:var(--cb-color-surface)}.cb-pdf__group{display:inline-flex;align-items:center;gap:var(--cb-space-1)}.cb-pdf__spacer{flex:1}.cb-pdf__tool{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;padding:0;border:none;border-radius:var(--cb-radius-md);background:transparent;color:var(--cb-color-text);cursor:pointer;transition:background-color var(--cb-duration-fast) var(--cb-ease)}.cb-pdf__tool:hover:not(:disabled){background:var(--cb-color-surface-hover)}.cb-pdf__tool[aria-expanded=true]{background:var(--cb-color-surface-hover);color:var(--cb-color-primary-600)}.cb-pdf__tool:disabled{opacity:var(--cb-opacity-disabled, .5);cursor:default}.cb-pdf__tool:focus-visible,.cb-pdf__field:focus-visible,.cb-pdf__thumb:focus-visible,.cb-pdf__submit:focus-visible,.cb-pdf__viewport:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-pdf__field{height:2.25rem;padding:0 var(--cb-space-2);border:var(--cb-border-width, 1px) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit}.cb-pdf__field:disabled{opacity:var(--cb-opacity-disabled, .5)}.cb-pdf__page-input{width:3.25rem;text-align:center}.cb-pdf__total{min-width:2.5rem;color:var(--cb-color-text-muted);white-space:nowrap}.cb-pdf__zoom{width:9.5rem}.cb-pdf__search{position:relative}.cb-pdf__search-icon{position:absolute;left:.625rem;color:var(--cb-color-text-muted);pointer-events:none}.cb-pdf__search-input{width:14rem;padding-left:2rem}.cb-pdf__status{min-width:4.5rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs);text-align:center;white-space:nowrap}@container (max-width: 40rem){.cb-pdf__zoom,.cb-pdf__status{display:none}.cb-pdf__search-input{width:9rem}}.cb-pdf__body{display:flex;flex:1;min-height:0}.cb-pdf__sidebar{display:flex;flex-direction:column;align-items:center;gap:var(--cb-space-3);flex:none;width:10rem;padding:var(--cb-space-3) 0;overflow-y:auto;border-right:var(--cb-border-width, 1px) solid var(--cb-color-border);background:var(--cb-color-surface)}.cb-pdf__thumb{display:flex;flex-direction:column;align-items:center;gap:var(--cb-space-1);padding:var(--cb-space-1);border:2px solid transparent;border-radius:var(--cb-radius-md);background:transparent;color:var(--cb-color-text-muted);font:inherit;font-size:var(--cb-text-xs);cursor:pointer}.cb-pdf__thumb:hover{background:var(--cb-color-surface-hover)}.cb-pdf__thumb.cb-is-active{border-color:var(--cb-color-primary-500);color:var(--cb-color-text-strong);font-weight:var(--cb-font-weight-semibold)}.cb-pdf__thumb-canvas{display:block;background:#fff;box-shadow:0 1px 3px #0f172a33}:host ::ng-deep .cb-pdf__thumb-canvas canvas{display:block;width:100%;height:100%}.cb-pdf__viewport{position:relative;flex:1;min-width:0;overflow:auto;background:var(--cb-color-surface-subtle)}.cb-pdf__pages{display:flex;flex-direction:column;align-items:center;gap:16px;padding:16px;width:max-content;min-width:100%;box-sizing:border-box}.cb-pdf__page{--user-unit: 1;--total-scale-factor: calc(var(--scale-factor) * var(--user-unit));--scale-round-x: 1px;--scale-round-y: 1px;position:relative;flex:none;background:#fff;box-shadow:0 1px 3px #0f172a2e,0 4px 12px #0f172a14;direction:ltr}.cb-pdf__canvas,:host ::ng-deep .cb-pdf__canvas canvas{position:absolute;inset:0;width:100%;height:100%}.cb-pdf__text{position:absolute;inset:0;overflow:clip;opacity:1;line-height:1;text-align:initial;letter-spacing:normal;word-spacing:normal;-webkit-text-size-adjust:none;text-size-adjust:none;forced-color-adjust:none;transform-origin:0 0;caret-color:CanvasText;z-index:0;--min-font-size: 1;--text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));--min-font-size-inv: calc(1 / var(--min-font-size))}.cb-pdf__text[data-main-rotation=\"90\"]{transform:rotate(90deg) translateY(-100%)}.cb-pdf__text[data-main-rotation=\"180\"]{transform:rotate(180deg) translate(-100%,-100%)}.cb-pdf__text[data-main-rotation=\"270\"]{transform:rotate(270deg) translate(-100%)}:host ::ng-deep .cb-pdf__text :is(span,br){position:absolute;color:transparent;white-space:pre;cursor:text;transform-origin:0% 0%;-webkit-user-select:text;user-select:text}:host ::ng-deep .cb-pdf__text>:not(.markedContent),:host ::ng-deep .cb-pdf__text .markedContent span:not(.markedContent){--font-height: 0;--scale-x: 1;--rotate: 0deg;z-index:1;font-size:calc(var(--text-scale-factor) * var(--font-height));transform:rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv))}:host ::ng-deep .cb-pdf__text .markedContent{display:contents}:host ::ng-deep .cb-pdf__text span[role=img]{-webkit-user-select:none;user-select:none;cursor:default}:host ::ng-deep .cb-pdf__text ::selection{background:color-mix(in srgb,var(--cb-color-primary-500) 35%,transparent);color:transparent}:host ::ng-deep .cb-pdf__text .cb-pdf__hl{position:relative;margin:-1px;padding:1px;border-radius:3px;background:#facc1573;font-size:inherit;transform:none}:host ::ng-deep .cb-pdf__text .cb-pdf__hl.cb-is-selected{background:#f973168c;outline:2px solid rgb(234 88 12)}@media(forced-colors:active){:host ::ng-deep .cb-pdf__text .cb-pdf__hl{background:transparent;outline:1px solid Highlight}:host ::ng-deep .cb-pdf__text .cb-pdf__hl.cb-is-selected{outline:3px solid Highlight}}.cb-pdf__message{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--cb-space-3);height:100%;padding:var(--cb-space-6);color:var(--cb-color-text-muted);text-align:center}.cb-pdf__message--error{color:var(--cb-color-danger)}.cb-pdf__password{display:flex;flex-direction:column;align-items:center;gap:var(--cb-space-2);max-width:24rem;margin:var(--cb-space-6) auto;padding:var(--cb-space-6);border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);text-align:center}.cb-pdf__password-text{margin:0}.cb-pdf__password-label{align-self:flex-start;font-weight:var(--cb-font-weight-medium)}.cb-pdf__password-row{display:flex;gap:var(--cb-space-2);width:100%}.cb-pdf__password-row .cb-pdf__field{flex:1;min-width:0}.cb-pdf__password-error{margin:0;color:var(--cb-color-danger)}.cb-pdf__submit{height:2.25rem;padding:0 var(--cb-space-4);border:none;border-radius:var(--cb-radius-md);background:var(--cb-color-primary-500);color:var(--cb-color-primary-contrast);font:inherit;font-weight:var(--cb-font-weight-semibold);cursor:pointer}.cb-pdf__submit:hover{background:var(--cb-color-primary-600)}.cb-pdf__toast{position:absolute;left:50%;bottom:var(--cb-space-4);transform:translate(-50%);padding:var(--cb-space-2) var(--cb-space-4);border-radius:var(--cb-radius-full);background:var(--cb-color-surface-950);color:#fff;box-shadow:var(--cb-shadow-md)}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }, { kind: "component", type: CbSpinner, selector: "cb-spinner", inputs: ["size", "strokeWidth", "severity", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1273
+ }
1274
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbPdfViewer, decorators: [{
1275
+ type: Component,
1276
+ args: [{ selector: 'cb-pdf-viewer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [CbIcon, CbSpinner], host: {
1277
+ class: 'cb-pdf-viewer',
1278
+ role: 'region',
1279
+ '[attr.aria-label]': 'ariaLabel() || t().pdfViewer',
1280
+ '(keydown)': 'onHostKeydown($event)',
1281
+ }, template: `
1282
+ @if (showToolbar()) {
1283
+ <div class="cb-pdf__toolbar">
1284
+ @if (showThumbnails()) {
1285
+ <div class="cb-pdf__group">
1286
+ <button
1287
+ type="button"
1288
+ class="cb-pdf__tool"
1289
+ [attr.aria-label]="t().thumbnails"
1290
+ [attr.aria-expanded]="sidebar()"
1291
+ [attr.aria-controls]="uid + '-thumbs'"
1292
+ [disabled]="state() !== 'ready'"
1293
+ (click)="sidebar.set(!sidebar())"
1294
+ >
1295
+ <cb-icon name="panel-left" [size]="18" aria-hidden="true" />
1296
+ </button>
1297
+ </div>
1298
+ }
1299
+
1300
+ <div class="cb-pdf__group" role="group" [attr.aria-label]="t().pagination">
1301
+ <button
1302
+ type="button"
1303
+ class="cb-pdf__tool"
1304
+ [attr.aria-label]="t().previousPage"
1305
+ [disabled]="state() !== 'ready' || page() <= 1"
1306
+ (click)="previousPage()"
1307
+ >
1308
+ <cb-icon name="chevron-up" [size]="18" aria-hidden="true" />
1309
+ </button>
1310
+ <button
1311
+ type="button"
1312
+ class="cb-pdf__tool"
1313
+ [attr.aria-label]="t().nextPage"
1314
+ [disabled]="state() !== 'ready' || page() >= numPages()"
1315
+ (click)="nextPage()"
1316
+ >
1317
+ <cb-icon name="chevron-down" [size]="18" aria-hidden="true" />
1318
+ </button>
1319
+ <input
1320
+ class="cb-pdf__field cb-pdf__page-input"
1321
+ type="text"
1322
+ inputmode="numeric"
1323
+ autocomplete="off"
1324
+ [id]="uid + '-page'"
1325
+ [attr.aria-label]="t().pageNumber"
1326
+ [attr.aria-describedby]="uid + '-total'"
1327
+ [value]="page()"
1328
+ [disabled]="state() !== 'ready'"
1329
+ (change)="onPageInput($event)"
1330
+ (keydown.enter)="onPageInput($event)"
1331
+ />
1332
+ <span class="cb-pdf__total" [id]="uid + '-total'">{{ format(t().ofTotal, numPages() || '–') }}</span>
1333
+ </div>
1334
+
1335
+ <div class="cb-pdf__group" role="group" [attr.aria-label]="t().zoomLevel">
1336
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().zoomOut" [disabled]="state() !== 'ready' || scale() <= 0.25" (click)="zoomOut()">
1337
+ <cb-icon name="zoom-out" [size]="18" aria-hidden="true" />
1338
+ </button>
1339
+ <select
1340
+ class="cb-pdf__field cb-pdf__zoom"
1341
+ [attr.aria-label]="t().zoomLevel"
1342
+ [disabled]="state() !== 'ready'"
1343
+ (change)="onZoomSelect($any($event.target).value)"
1344
+ >
1345
+ <option value="page-width" [selected]="zoomValue() === 'page-width'">{{ t().fitWidth }}</option>
1346
+ <option value="page-fit" [selected]="zoomValue() === 'page-fit'">{{ t().fitPage }}</option>
1347
+ @if (customZoom(); as custom) {
1348
+ <option [value]="custom" selected>{{ percent(custom) }}</option>
1349
+ }
1350
+ @for (level of zoomLevels; track level) {
1351
+ <option [value]="level" [selected]="zoomValue() === '' + level">{{ percent(level) }}</option>
1352
+ }
1353
+ </select>
1354
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().zoomIn" [disabled]="state() !== 'ready' || scale() >= 5" (click)="zoomIn()">
1355
+ <cb-icon name="zoom-in" [size]="18" aria-hidden="true" />
1356
+ </button>
1357
+ @if (showRotate()) {
1358
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().rotateRight" [disabled]="state() !== 'ready'" (click)="rotate()">
1359
+ <cb-icon name="rotate-cw" [size]="18" aria-hidden="true" />
1360
+ </button>
1361
+ }
1362
+ </div>
1363
+
1364
+ <span class="cb-pdf__spacer"></span>
1365
+
1366
+ @if (showSearch()) {
1367
+ <div class="cb-pdf__group cb-pdf__search" role="search">
1368
+ <cb-icon class="cb-pdf__search-icon" name="search" [size]="16" aria-hidden="true" />
1369
+ <input
1370
+ #searchInput
1371
+ class="cb-pdf__field cb-pdf__search-input"
1372
+ type="search"
1373
+ autocomplete="off"
1374
+ [attr.aria-label]="t().searchInDocument"
1375
+ [attr.aria-describedby]="uid + '-status'"
1376
+ [placeholder]="t().searchInDocument"
1377
+ [disabled]="state() !== 'ready'"
1378
+ (input)="onSearchInput(searchInput.value)"
1379
+ (keydown)="onSearchKeydown($event, searchInput.value)"
1380
+ />
1381
+ <span class="cb-pdf__status" [id]="uid + '-status'" aria-live="polite">{{ searchStatus() }}</span>
1382
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().previousMatch" [disabled]="!matches().length" (click)="findPrevious()">
1383
+ <cb-icon name="chevron-left" [size]="18" aria-hidden="true" />
1384
+ </button>
1385
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().nextMatch" [disabled]="!matches().length" (click)="findNext()">
1386
+ <cb-icon name="chevron-right" [size]="18" aria-hidden="true" />
1387
+ </button>
1388
+ </div>
1389
+ }
1390
+
1391
+ @if (downloadable() || printable()) {
1392
+ <div class="cb-pdf__group">
1393
+ @if (downloadable()) {
1394
+ <button type="button" class="cb-pdf__tool" [attr.aria-label]="t().download" [disabled]="state() !== 'ready'" (click)="download()">
1395
+ <cb-icon name="download" [size]="18" aria-hidden="true" />
1396
+ </button>
1397
+ }
1398
+ @if (printable()) {
1399
+ <button
1400
+ type="button"
1401
+ class="cb-pdf__tool"
1402
+ [attr.aria-label]="t().print"
1403
+ [disabled]="state() !== 'ready' || !!printProgress()"
1404
+ (click)="print()"
1405
+ >
1406
+ <cb-icon name="printer" [size]="18" aria-hidden="true" />
1407
+ </button>
1408
+ }
1409
+ </div>
1410
+ }
1411
+ </div>
1412
+ }
1413
+
1414
+ <div class="cb-pdf__body">
1415
+ @if (state() === 'ready' && sidebar() && showThumbnails()) {
1416
+ <nav class="cb-pdf__sidebar" [id]="uid + '-thumbs'" [attr.aria-label]="t().thumbnails">
1417
+ @for (thumb of thumbBoxes(); track $index; let i = $index) {
1418
+ <button
1419
+ #thumbEl
1420
+ type="button"
1421
+ class="cb-pdf__thumb"
1422
+ [class.cb-is-active]="page() === i + 1"
1423
+ [attr.data-page]="i + 1"
1424
+ [attr.aria-label]="format(t().pageOf, i + 1, numPages())"
1425
+ [attr.aria-current]="page() === i + 1 ? 'page' : null"
1426
+ (click)="goToPage(i + 1)"
1427
+ (keydown)="onThumbKeydown($event, i + 1)"
1428
+ >
1429
+ <span class="cb-pdf__thumb-canvas" [style.width.px]="thumb.width" [style.height.px]="thumb.height"></span>
1430
+ <span class="cb-pdf__thumb-label" aria-hidden="true">{{ i + 1 }}</span>
1431
+ </button>
1432
+ }
1433
+ </nav>
1434
+ }
1435
+
1436
+ <div
1437
+ #viewport
1438
+ class="cb-pdf__viewport"
1439
+ tabindex="0"
1440
+ role="document"
1441
+ [attr.aria-label]="t().pdfDocument"
1442
+ [attr.aria-busy]="state() === 'loading' ? 'true' : null"
1443
+ (scroll)="onScroll()"
1444
+ (wheel)="onWheel($event)"
1445
+ >
1446
+ @switch (state()) {
1447
+ @case ('loading') {
1448
+ <div class="cb-pdf__message" role="status">
1449
+ <cb-spinner [size]="32" aria-hidden="true" />
1450
+ <span>{{ t().loadingDocument }}@if (progress() !== null) { {{ progress() }} %}</span>
1451
+ </div>
1452
+ }
1453
+ @case ('error') {
1454
+ <div class="cb-pdf__message cb-pdf__message--error" role="alert">{{ t().documentError }}</div>
1455
+ }
1456
+ @case ('password') {
1457
+ <form class="cb-pdf__password" (submit)="submitPassword($event, passwordInput.value)">
1458
+ <cb-icon name="lock" [size]="28" aria-hidden="true" />
1459
+ <p class="cb-pdf__password-text" [id]="uid + '-pwd-text'">{{ t().protectedDocument }}</p>
1460
+ <label class="cb-pdf__password-label" [for]="uid + '-pwd'">{{ t().password }}</label>
1461
+ <div class="cb-pdf__password-row">
1462
+ <input
1463
+ #passwordInput
1464
+ class="cb-pdf__field"
1465
+ type="password"
1466
+ autocomplete="off"
1467
+ [id]="uid + '-pwd'"
1468
+ [attr.aria-describedby]="passwordIncorrect() ? uid + '-pwd-error' : uid + '-pwd-text'"
1469
+ [attr.aria-invalid]="passwordIncorrect() ? 'true' : null"
1470
+ />
1471
+ <button type="submit" class="cb-pdf__submit">{{ t().open }}</button>
1472
+ </div>
1473
+ @if (passwordIncorrect()) {
1474
+ <p class="cb-pdf__password-error" [id]="uid + '-pwd-error'" role="alert">{{ t().incorrectPassword }}</p>
1475
+ }
1476
+ </form>
1477
+ }
1478
+ @case ('ready') {
1479
+ <div class="cb-pdf__pages">
1480
+ @for (box of pageBoxes(); track $index; let i = $index) {
1481
+ <div
1482
+ #pageEl
1483
+ class="cb-pdf__page"
1484
+ role="group"
1485
+ [attr.data-page]="i + 1"
1486
+ [attr.aria-label]="format(t().pageOf, i + 1, numPages())"
1487
+ [style.width.px]="box.width"
1488
+ [style.height.px]="box.height"
1489
+ [style.--scale-factor]="box.scaleFactor"
1490
+ [style.--user-unit]="box.userUnit"
1491
+ >
1492
+ <div class="cb-pdf__canvas"></div>
1493
+ <div class="cb-pdf__text"></div>
1494
+ </div>
1495
+ }
1496
+ </div>
1497
+ }
1498
+ }
1499
+ </div>
1500
+ </div>
1501
+
1502
+ @if (printProgress(); as p) {
1503
+ <div class="cb-pdf__toast" role="status">{{ format(t().preparingPrint, p.done, p.total) }}</div>
1504
+ }
1505
+ `, styles: [":host{position:relative;display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:var(--cb-pdf-viewer-height, 40rem);min-height:16rem;overflow:hidden;border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);color:var(--cb-color-text);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm);container-type:inline-size}.cb-pdf__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-2);padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width, 1px) solid var(--cb-color-border);background:var(--cb-color-surface)}.cb-pdf__group{display:inline-flex;align-items:center;gap:var(--cb-space-1)}.cb-pdf__spacer{flex:1}.cb-pdf__tool{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;padding:0;border:none;border-radius:var(--cb-radius-md);background:transparent;color:var(--cb-color-text);cursor:pointer;transition:background-color var(--cb-duration-fast) var(--cb-ease)}.cb-pdf__tool:hover:not(:disabled){background:var(--cb-color-surface-hover)}.cb-pdf__tool[aria-expanded=true]{background:var(--cb-color-surface-hover);color:var(--cb-color-primary-600)}.cb-pdf__tool:disabled{opacity:var(--cb-opacity-disabled, .5);cursor:default}.cb-pdf__tool:focus-visible,.cb-pdf__field:focus-visible,.cb-pdf__thumb:focus-visible,.cb-pdf__submit:focus-visible,.cb-pdf__viewport:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-pdf__field{height:2.25rem;padding:0 var(--cb-space-2);border:var(--cb-border-width, 1px) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit}.cb-pdf__field:disabled{opacity:var(--cb-opacity-disabled, .5)}.cb-pdf__page-input{width:3.25rem;text-align:center}.cb-pdf__total{min-width:2.5rem;color:var(--cb-color-text-muted);white-space:nowrap}.cb-pdf__zoom{width:9.5rem}.cb-pdf__search{position:relative}.cb-pdf__search-icon{position:absolute;left:.625rem;color:var(--cb-color-text-muted);pointer-events:none}.cb-pdf__search-input{width:14rem;padding-left:2rem}.cb-pdf__status{min-width:4.5rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs);text-align:center;white-space:nowrap}@container (max-width: 40rem){.cb-pdf__zoom,.cb-pdf__status{display:none}.cb-pdf__search-input{width:9rem}}.cb-pdf__body{display:flex;flex:1;min-height:0}.cb-pdf__sidebar{display:flex;flex-direction:column;align-items:center;gap:var(--cb-space-3);flex:none;width:10rem;padding:var(--cb-space-3) 0;overflow-y:auto;border-right:var(--cb-border-width, 1px) solid var(--cb-color-border);background:var(--cb-color-surface)}.cb-pdf__thumb{display:flex;flex-direction:column;align-items:center;gap:var(--cb-space-1);padding:var(--cb-space-1);border:2px solid transparent;border-radius:var(--cb-radius-md);background:transparent;color:var(--cb-color-text-muted);font:inherit;font-size:var(--cb-text-xs);cursor:pointer}.cb-pdf__thumb:hover{background:var(--cb-color-surface-hover)}.cb-pdf__thumb.cb-is-active{border-color:var(--cb-color-primary-500);color:var(--cb-color-text-strong);font-weight:var(--cb-font-weight-semibold)}.cb-pdf__thumb-canvas{display:block;background:#fff;box-shadow:0 1px 3px #0f172a33}:host ::ng-deep .cb-pdf__thumb-canvas canvas{display:block;width:100%;height:100%}.cb-pdf__viewport{position:relative;flex:1;min-width:0;overflow:auto;background:var(--cb-color-surface-subtle)}.cb-pdf__pages{display:flex;flex-direction:column;align-items:center;gap:16px;padding:16px;width:max-content;min-width:100%;box-sizing:border-box}.cb-pdf__page{--user-unit: 1;--total-scale-factor: calc(var(--scale-factor) * var(--user-unit));--scale-round-x: 1px;--scale-round-y: 1px;position:relative;flex:none;background:#fff;box-shadow:0 1px 3px #0f172a2e,0 4px 12px #0f172a14;direction:ltr}.cb-pdf__canvas,:host ::ng-deep .cb-pdf__canvas canvas{position:absolute;inset:0;width:100%;height:100%}.cb-pdf__text{position:absolute;inset:0;overflow:clip;opacity:1;line-height:1;text-align:initial;letter-spacing:normal;word-spacing:normal;-webkit-text-size-adjust:none;text-size-adjust:none;forced-color-adjust:none;transform-origin:0 0;caret-color:CanvasText;z-index:0;--min-font-size: 1;--text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));--min-font-size-inv: calc(1 / var(--min-font-size))}.cb-pdf__text[data-main-rotation=\"90\"]{transform:rotate(90deg) translateY(-100%)}.cb-pdf__text[data-main-rotation=\"180\"]{transform:rotate(180deg) translate(-100%,-100%)}.cb-pdf__text[data-main-rotation=\"270\"]{transform:rotate(270deg) translate(-100%)}:host ::ng-deep .cb-pdf__text :is(span,br){position:absolute;color:transparent;white-space:pre;cursor:text;transform-origin:0% 0%;-webkit-user-select:text;user-select:text}:host ::ng-deep .cb-pdf__text>:not(.markedContent),:host ::ng-deep .cb-pdf__text .markedContent span:not(.markedContent){--font-height: 0;--scale-x: 1;--rotate: 0deg;z-index:1;font-size:calc(var(--text-scale-factor) * var(--font-height));transform:rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv))}:host ::ng-deep .cb-pdf__text .markedContent{display:contents}:host ::ng-deep .cb-pdf__text span[role=img]{-webkit-user-select:none;user-select:none;cursor:default}:host ::ng-deep .cb-pdf__text ::selection{background:color-mix(in srgb,var(--cb-color-primary-500) 35%,transparent);color:transparent}:host ::ng-deep .cb-pdf__text .cb-pdf__hl{position:relative;margin:-1px;padding:1px;border-radius:3px;background:#facc1573;font-size:inherit;transform:none}:host ::ng-deep .cb-pdf__text .cb-pdf__hl.cb-is-selected{background:#f973168c;outline:2px solid rgb(234 88 12)}@media(forced-colors:active){:host ::ng-deep .cb-pdf__text .cb-pdf__hl{background:transparent;outline:1px solid Highlight}:host ::ng-deep .cb-pdf__text .cb-pdf__hl.cb-is-selected{outline:3px solid Highlight}}.cb-pdf__message{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--cb-space-3);height:100%;padding:var(--cb-space-6);color:var(--cb-color-text-muted);text-align:center}.cb-pdf__message--error{color:var(--cb-color-danger)}.cb-pdf__password{display:flex;flex-direction:column;align-items:center;gap:var(--cb-space-2);max-width:24rem;margin:var(--cb-space-6) auto;padding:var(--cb-space-6);border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);text-align:center}.cb-pdf__password-text{margin:0}.cb-pdf__password-label{align-self:flex-start;font-weight:var(--cb-font-weight-medium)}.cb-pdf__password-row{display:flex;gap:var(--cb-space-2);width:100%}.cb-pdf__password-row .cb-pdf__field{flex:1;min-width:0}.cb-pdf__password-error{margin:0;color:var(--cb-color-danger)}.cb-pdf__submit{height:2.25rem;padding:0 var(--cb-space-4);border:none;border-radius:var(--cb-radius-md);background:var(--cb-color-primary-500);color:var(--cb-color-primary-contrast);font:inherit;font-weight:var(--cb-font-weight-semibold);cursor:pointer}.cb-pdf__submit:hover{background:var(--cb-color-primary-600)}.cb-pdf__toast{position:absolute;left:50%;bottom:var(--cb-space-4);transform:translate(-50%);padding:var(--cb-space-2) var(--cb-space-4);border-radius:var(--cb-radius-full);background:var(--cb-color-surface-950);color:#fff;box-shadow:var(--cb-shadow-md)}\n"] }]
1506
+ }], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], fileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileName", required: false }] }], password: [{ type: i0.Input, args: [{ isSignal: true, alias: "password", required: false }] }], withCredentials: [{ type: i0.Input, args: [{ isSignal: true, alias: "withCredentials", required: false }] }], httpHeaders: [{ type: i0.Input, args: [{ isSignal: true, alias: "httpHeaders", required: false }] }], page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }, { type: i0.Output, args: ["pageChange"] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }, { type: i0.Output, args: ["zoomChange"] }], rotation: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotation", required: false }] }, { type: i0.Output, args: ["rotationChange"] }], sidebar: [{ type: i0.Input, args: [{ isSignal: true, alias: "sidebar", required: false }] }, { type: i0.Output, args: ["sidebarChange"] }], showToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToolbar", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], showThumbnails: [{ type: i0.Input, args: [{ isSignal: true, alias: "showThumbnails", required: false }] }], showRotate: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRotate", required: false }] }], downloadable: [{ type: i0.Input, args: [{ isSignal: true, alias: "downloadable", required: false }] }], printable: [{ type: i0.Input, args: [{ isSignal: true, alias: "printable", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], loadError: [{ type: i0.Output, args: ["loadError"] }], viewport: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], pageEls: [{ type: i0.ViewChildren, args: ['pageEl', { isSignal: true }] }], thumbEls: [{ type: i0.ViewChildren, args: ['thumbEl', { isSignal: true }] }], searchInput: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }], passwordInput: [{ type: i0.ViewChild, args: ['passwordInput', { isSignal: true }] }] } });
1507
+ function sizeOf(page) {
1508
+ const viewport = page.getViewport({ scale: 1, rotation: 0 });
1509
+ const userUnit = page.userUnit || 1;
1510
+ return { width: viewport.width, height: viewport.height, rotate: page.rotate || 0, userUnit };
1511
+ }
1512
+ /** Bytes del documento si no es una URL (copiados: pdf.js transfiere el buffer al worker). */
1513
+ async function toData(src) {
1514
+ if (src instanceof Uint8Array)
1515
+ return src.slice();
1516
+ if (src instanceof ArrayBuffer)
1517
+ return new Uint8Array(src.slice(0));
1518
+ if (typeof Blob !== 'undefined' && src instanceof Blob)
1519
+ return new Uint8Array(await readBlob(src));
1520
+ return null;
1521
+ }
1522
+ function readBlob(blob) {
1523
+ if (typeof blob.arrayBuffer === 'function')
1524
+ return blob.arrayBuffer();
1525
+ return new Promise((resolve, reject) => {
1526
+ const reader = new FileReader();
1527
+ reader.onload = () => resolve(reader.result);
1528
+ reader.onerror = () => reject(reader.error);
1529
+ reader.readAsArrayBuffer(blob);
1530
+ });
1531
+ }
1532
+ /** Densidad del canvas: la de la pantalla, limitada para no pasar de {@link MAX_CANVAS_PIXELS}. */
1533
+ function outputScale(width, height) {
1534
+ const ratio = globalThis.devicePixelRatio || 1;
1535
+ const max = Math.sqrt(MAX_CANVAS_PIXELS / Math.max(1, width * height));
1536
+ return Math.max(0.5, Math.min(ratio, max));
1537
+ }
1538
+ function clamp(value, min, max) {
1539
+ return Math.min(max, Math.max(min, value));
1540
+ }
1541
+ function round2(value) {
1542
+ return Math.round(value * 100) / 100;
1543
+ }
1544
+
1545
+ /*
1546
+ * Public API Surface de @caribeng/ui/pdfviewer
1547
+ */
1548
+
1549
+ /**
1550
+ * Generated bundle index. Do not edit.
1551
+ */
1552
+
1553
+ export { CB_PDF_VIEWER_OPTIONS, CbPdfEngine, CbPdfViewer, provideCbPdfViewer };