@pallasoft/ps-file-components 2.3.0 → 2.4.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 (152) hide show
  1. package/dist/components/ps-file-search-bar.d.ts +10 -0
  2. package/dist/components/ps-file-search-bar.js +25 -0
  3. package/dist/components/ps-file-viewer-content.js +9 -0
  4. package/dist/components/ps-file-viewer-pane.d.ts +1 -1
  5. package/dist/components/ps-file-viewer-pane.js +26 -2
  6. package/dist/components/ps-file-viewer.js +11 -23
  7. package/dist/components/ps-html-document.d.ts +8 -0
  8. package/dist/components/ps-html-document.js +23 -0
  9. package/dist/components/ps-html-viewer.d.ts +2 -0
  10. package/dist/components/ps-html-viewer.js +17 -0
  11. package/dist/components/ps-image-text-highlights.d.ts +9 -0
  12. package/dist/components/ps-image-text-highlights.js +31 -0
  13. package/dist/components/ps-image-viewer.d.ts +1 -1
  14. package/dist/components/ps-image-viewer.js +11 -2
  15. package/dist/components/ps-pdf-page.js +1 -1
  16. package/dist/components/ps-pdf-viewer.js +6 -2
  17. package/dist/components/ps-sheet-viewer.d.ts +2 -0
  18. package/dist/components/ps-sheet-viewer.js +26 -0
  19. package/dist/components/ps-table-view.d.ts +7 -0
  20. package/dist/components/ps-table-view.js +17 -0
  21. package/dist/components/ps-text-viewer.js +7 -14
  22. package/dist/components/ps-word-viewer.d.ts +2 -0
  23. package/dist/components/ps-word-viewer.js +17 -0
  24. package/dist/constants/ps-file-kind-icons.js +3 -0
  25. package/dist/constants/ps-file-kind.d.ts +3 -0
  26. package/dist/constants/ps-file-kind.js +3 -0
  27. package/dist/constants/ps-file-type-config.d.ts +6 -1
  28. package/dist/constants/ps-file-type-config.js +16 -2
  29. package/dist/constants/ps-file-viewer-config.d.ts +1 -0
  30. package/dist/constants/ps-file-viewer-config.js +1 -0
  31. package/dist/constants/ps-file-viewer-kinds.d.ts +2 -2
  32. package/dist/constants/ps-file-viewer-kinds.js +17 -2
  33. package/dist/constants/ps-file-viewer-labels.d.ts +3 -0
  34. package/dist/constants/ps-file-viewer-labels.js +3 -0
  35. package/dist/constants/ps-html-sanitize-config.d.ts +8 -0
  36. package/dist/constants/ps-html-sanitize-config.js +26 -0
  37. package/dist/esm/components/ps-file-search-bar.js +21 -0
  38. package/dist/esm/components/ps-file-viewer-content.js +9 -0
  39. package/dist/esm/components/ps-file-viewer-pane.js +27 -3
  40. package/dist/esm/components/ps-file-viewer.js +13 -25
  41. package/dist/esm/components/ps-html-document.js +19 -0
  42. package/dist/esm/components/ps-html-viewer.js +13 -0
  43. package/dist/esm/components/ps-image-text-highlights.js +27 -0
  44. package/dist/esm/components/ps-image-viewer.js +13 -4
  45. package/dist/esm/components/ps-pdf-page.js +1 -1
  46. package/dist/esm/components/ps-pdf-viewer.js +6 -2
  47. package/dist/esm/components/ps-sheet-viewer.js +22 -0
  48. package/dist/esm/components/ps-table-view.js +13 -0
  49. package/dist/esm/components/ps-text-viewer.js +7 -14
  50. package/dist/esm/components/ps-word-viewer.js +13 -0
  51. package/dist/esm/constants/ps-file-kind-icons.js +3 -0
  52. package/dist/esm/constants/ps-file-kind.js +3 -0
  53. package/dist/esm/constants/ps-file-type-config.js +16 -2
  54. package/dist/esm/constants/ps-file-viewer-config.js +1 -0
  55. package/dist/esm/constants/ps-file-viewer-kinds.js +17 -2
  56. package/dist/esm/constants/ps-file-viewer-labels.js +3 -0
  57. package/dist/esm/constants/ps-html-sanitize-config.js +23 -0
  58. package/dist/esm/hooks/use-ps-file-search.js +37 -0
  59. package/dist/esm/hooks/use-ps-file-text.js +4 -19
  60. package/dist/esm/hooks/use-ps-find-shortcut.js +23 -0
  61. package/dist/esm/hooks/use-ps-loaded-file.js +19 -0
  62. package/dist/esm/index.js +33 -0
  63. package/dist/esm/models/ps-file-search-state.js +1 -0
  64. package/dist/esm/models/ps-html-highlight-result.js +1 -0
  65. package/dist/esm/models/ps-html-text-node.js +1 -0
  66. package/dist/esm/models/ps-image-match.js +1 -0
  67. package/dist/esm/models/ps-image-text-box.js +1 -0
  68. package/dist/esm/models/ps-image-text-layer.js +1 -0
  69. package/dist/esm/models/ps-image-text-word.js +1 -0
  70. package/dist/esm/models/ps-item-match-part.js +1 -0
  71. package/dist/esm/models/ps-item-match.js +1 -0
  72. package/dist/esm/models/ps-loaded-file-state.js +1 -0
  73. package/dist/esm/models/ps-normalized-text.js +1 -0
  74. package/dist/esm/models/ps-pdf-text-item.js +1 -0
  75. package/dist/esm/models/ps-pdf-viewport-like.js +1 -0
  76. package/dist/esm/models/ps-search-item.js +1 -0
  77. package/dist/esm/models/ps-sheet-data.js +1 -0
  78. package/dist/esm/services/ps-file-fetch-service.js +14 -0
  79. package/dist/esm/services/ps-file-search-service.js +15 -0
  80. package/dist/esm/services/ps-file-type-service.js +12 -1
  81. package/dist/esm/services/ps-html-file-service.js +7 -0
  82. package/dist/esm/services/ps-html-service.js +83 -0
  83. package/dist/esm/services/ps-image-text-service.js +28 -0
  84. package/dist/esm/services/ps-label-service.js +5 -0
  85. package/dist/esm/services/ps-pdf-service.js +39 -16
  86. package/dist/esm/services/ps-sheet-service.js +23 -0
  87. package/dist/esm/services/ps-text-search-service.js +70 -5
  88. package/dist/esm/services/ps-word-service.js +15 -0
  89. package/dist/hooks/use-ps-file-search.d.ts +2 -0
  90. package/dist/hooks/use-ps-file-search.js +41 -0
  91. package/dist/hooks/use-ps-file-text.js +4 -19
  92. package/dist/hooks/use-ps-find-shortcut.d.ts +2 -0
  93. package/dist/hooks/use-ps-find-shortcut.js +27 -0
  94. package/dist/hooks/use-ps-loaded-file.d.ts +2 -0
  95. package/dist/hooks/use-ps-loaded-file.js +23 -0
  96. package/dist/index.d.ts +33 -0
  97. package/dist/index.js +33 -0
  98. package/dist/models/ps-file-search-state.d.ts +13 -0
  99. package/dist/models/ps-file-search-state.js +2 -0
  100. package/dist/models/ps-file-viewer-pane-props.d.ts +3 -1
  101. package/dist/models/ps-html-highlight-result.d.ts +4 -0
  102. package/dist/models/ps-html-highlight-result.js +2 -0
  103. package/dist/models/ps-html-text-node.d.ts +4 -0
  104. package/dist/models/ps-html-text-node.js +2 -0
  105. package/dist/models/ps-image-match.d.ts +7 -0
  106. package/dist/models/ps-image-match.js +2 -0
  107. package/dist/models/ps-image-text-box.d.ts +6 -0
  108. package/dist/models/ps-image-text-box.js +2 -0
  109. package/dist/models/ps-image-text-layer.d.ts +6 -0
  110. package/dist/models/ps-image-text-layer.js +2 -0
  111. package/dist/models/ps-image-text-word.d.ts +5 -0
  112. package/dist/models/ps-image-text-word.js +2 -0
  113. package/dist/models/ps-item-match-part.d.ts +5 -0
  114. package/dist/models/ps-item-match-part.js +2 -0
  115. package/dist/models/ps-item-match.d.ts +4 -0
  116. package/dist/models/ps-item-match.js +2 -0
  117. package/dist/models/ps-loaded-file-state.d.ts +4 -0
  118. package/dist/models/ps-loaded-file-state.js +2 -0
  119. package/dist/models/ps-normalized-text.d.ts +5 -0
  120. package/dist/models/ps-normalized-text.js +2 -0
  121. package/dist/models/ps-pdf-match.d.ts +1 -0
  122. package/dist/models/ps-pdf-text-item.d.ts +7 -0
  123. package/dist/models/ps-pdf-text-item.js +2 -0
  124. package/dist/models/ps-pdf-viewport-like.d.ts +5 -0
  125. package/dist/models/ps-pdf-viewport-like.js +2 -0
  126. package/dist/models/ps-search-item.d.ts +4 -0
  127. package/dist/models/ps-search-item.js +2 -0
  128. package/dist/models/ps-sheet-data.d.ts +4 -0
  129. package/dist/models/ps-sheet-data.js +2 -0
  130. package/dist/models/ps-viewer-file.d.ts +2 -0
  131. package/dist/services/ps-file-fetch-service.d.ts +5 -0
  132. package/dist/services/ps-file-fetch-service.js +18 -0
  133. package/dist/services/ps-file-search-service.d.ts +5 -0
  134. package/dist/services/ps-file-search-service.js +19 -0
  135. package/dist/services/ps-file-type-service.js +12 -1
  136. package/dist/services/ps-html-file-service.d.ts +3 -0
  137. package/dist/services/ps-html-file-service.js +11 -0
  138. package/dist/services/ps-html-service.d.ts +7 -0
  139. package/dist/services/ps-html-service.js +120 -0
  140. package/dist/services/ps-image-text-service.d.ts +5 -0
  141. package/dist/services/ps-image-text-service.js +32 -0
  142. package/dist/services/ps-label-service.d.ts +3 -0
  143. package/dist/services/ps-label-service.js +9 -0
  144. package/dist/services/ps-pdf-service.d.ts +3 -0
  145. package/dist/services/ps-pdf-service.js +39 -16
  146. package/dist/services/ps-sheet-service.d.ts +5 -0
  147. package/dist/services/ps-sheet-service.js +60 -0
  148. package/dist/services/ps-text-search-service.d.ts +7 -0
  149. package/dist/services/ps-text-search-service.js +70 -5
  150. package/dist/services/ps-word-service.d.ts +3 -0
  151. package/dist/services/ps-word-service.js +52 -0
  152. package/package.json +5 -2
@@ -1,6 +1,11 @@
1
1
  import { PsFileKind } from './ps-file-kind';
2
2
  export declare const PsFileTypeConfig: {
3
- readonly csvTypes: readonly ["text/csv", "application/csv", "application/vnd.ms-excel"];
3
+ readonly csvTypes: readonly ["text/csv", "application/csv"];
4
+ readonly excelLegacyType: "application/vnd.ms-excel";
4
5
  readonly extensionKinds: Record<string, PsFileKind>;
6
+ readonly htmlTypes: readonly ["text/html", "application/xhtml+xml"];
7
+ readonly sheetTypes: readonly ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"];
5
8
  readonly textTypes: readonly ["application/json", "application/xml"];
9
+ readonly unsupportedImageTypes: readonly ["image/heic", "image/heif", "image/heic-sequence", "image/heif-sequence"];
10
+ readonly wordTypes: readonly ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"];
6
11
  };
@@ -3,27 +3,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsFileTypeConfig = void 0;
4
4
  const ps_file_kind_1 = require("./ps-file-kind");
5
5
  exports.PsFileTypeConfig = {
6
- csvTypes: ['text/csv', 'application/csv', 'application/vnd.ms-excel'],
6
+ csvTypes: ['text/csv', 'application/csv'],
7
+ excelLegacyType: 'application/vnd.ms-excel',
7
8
  extensionKinds: {
9
+ aac: ps_file_kind_1.PsFileKind.audio,
10
+ avif: ps_file_kind_1.PsFileKind.image,
11
+ bmp: ps_file_kind_1.PsFileKind.image,
8
12
  csv: ps_file_kind_1.PsFileKind.csv,
13
+ docx: ps_file_kind_1.PsFileKind.word,
9
14
  gif: ps_file_kind_1.PsFileKind.image,
10
- heic: ps_file_kind_1.PsFileKind.image,
15
+ htm: ps_file_kind_1.PsFileKind.html,
16
+ html: ps_file_kind_1.PsFileKind.html,
11
17
  jpeg: ps_file_kind_1.PsFileKind.image,
12
18
  jpg: ps_file_kind_1.PsFileKind.image,
13
19
  json: ps_file_kind_1.PsFileKind.text,
14
20
  log: ps_file_kind_1.PsFileKind.text,
21
+ m4a: ps_file_kind_1.PsFileKind.audio,
15
22
  md: ps_file_kind_1.PsFileKind.text,
16
23
  mov: ps_file_kind_1.PsFileKind.video,
17
24
  mp3: ps_file_kind_1.PsFileKind.audio,
18
25
  mp4: ps_file_kind_1.PsFileKind.video,
26
+ ogg: ps_file_kind_1.PsFileKind.audio,
19
27
  pdf: ps_file_kind_1.PsFileKind.pdf,
20
28
  png: ps_file_kind_1.PsFileKind.image,
21
29
  svg: ps_file_kind_1.PsFileKind.image,
30
+ tsv: ps_file_kind_1.PsFileKind.text,
22
31
  txt: ps_file_kind_1.PsFileKind.text,
23
32
  wav: ps_file_kind_1.PsFileKind.audio,
24
33
  webm: ps_file_kind_1.PsFileKind.video,
25
34
  webp: ps_file_kind_1.PsFileKind.image,
35
+ xlsx: ps_file_kind_1.PsFileKind.sheet,
26
36
  xml: ps_file_kind_1.PsFileKind.text,
27
37
  },
38
+ htmlTypes: ['text/html', 'application/xhtml+xml'],
39
+ sheetTypes: ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
28
40
  textTypes: ['application/json', 'application/xml'],
41
+ unsupportedImageTypes: ['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence'],
42
+ wordTypes: ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
29
43
  };
@@ -1,5 +1,6 @@
1
1
  export declare const PsFileViewerConfig: {
2
2
  readonly dragActivationDistance: 4;
3
+ readonly maxSheetRows: 2000;
3
4
  readonly maxZoom: 4;
4
5
  readonly minZoom: 0.25;
5
6
  readonly pdfBaseScale: 1.4;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsFileViewerConfig = void 0;
4
4
  exports.PsFileViewerConfig = {
5
5
  dragActivationDistance: 4,
6
+ maxSheetRows: 2000,
6
7
  maxZoom: 4,
7
8
  minZoom: 0.25,
8
9
  pdfBaseScale: 1.4,
@@ -1,5 +1,5 @@
1
1
  export declare const PsFileViewerKinds: {
2
2
  readonly rotatable: readonly ["image", "pdf"];
3
- readonly searchable: readonly ["csv", "pdf", "text"];
4
- readonly zoomable: readonly ["csv", "image", "pdf", "text"];
3
+ readonly searchable: readonly ["csv", "html", "pdf", "sheet", "text", "word"];
4
+ readonly zoomable: readonly ["csv", "html", "image", "pdf", "sheet", "text", "word"];
5
5
  };
@@ -4,6 +4,21 @@ exports.PsFileViewerKinds = void 0;
4
4
  const ps_file_kind_1 = require("./ps-file-kind");
5
5
  exports.PsFileViewerKinds = {
6
6
  rotatable: [ps_file_kind_1.PsFileKind.image, ps_file_kind_1.PsFileKind.pdf],
7
- searchable: [ps_file_kind_1.PsFileKind.csv, ps_file_kind_1.PsFileKind.pdf, ps_file_kind_1.PsFileKind.text],
8
- zoomable: [ps_file_kind_1.PsFileKind.csv, ps_file_kind_1.PsFileKind.image, ps_file_kind_1.PsFileKind.pdf, ps_file_kind_1.PsFileKind.text],
7
+ searchable: [
8
+ ps_file_kind_1.PsFileKind.csv,
9
+ ps_file_kind_1.PsFileKind.html,
10
+ ps_file_kind_1.PsFileKind.pdf,
11
+ ps_file_kind_1.PsFileKind.sheet,
12
+ ps_file_kind_1.PsFileKind.text,
13
+ ps_file_kind_1.PsFileKind.word,
14
+ ],
15
+ zoomable: [
16
+ ps_file_kind_1.PsFileKind.csv,
17
+ ps_file_kind_1.PsFileKind.html,
18
+ ps_file_kind_1.PsFileKind.image,
19
+ ps_file_kind_1.PsFileKind.pdf,
20
+ ps_file_kind_1.PsFileKind.sheet,
21
+ ps_file_kind_1.PsFileKind.text,
22
+ ps_file_kind_1.PsFileKind.word,
23
+ ],
9
24
  };
@@ -1,11 +1,13 @@
1
1
  export declare const PsFileViewerLabels: {
2
2
  readonly close: "Close";
3
+ readonly closeSearch: "Close search";
3
4
  readonly delete: "Delete";
4
5
  readonly deleteConfirm: "Delete file";
5
6
  readonly deleteConfirmBody: "will be deleted. You cannot undo this.";
6
7
  readonly deleteCancel: "Cancel";
7
8
  readonly download: "Download";
8
9
  readonly loadFailed: "This file could not be opened.";
10
+ readonly matchCount: "{current} of {total}";
9
11
  readonly next: "Next file";
10
12
  readonly nextMatch: "Next match";
11
13
  readonly noMatches: "No matches";
@@ -14,6 +16,7 @@ export declare const PsFileViewerLabels: {
14
16
  readonly previousMatch: "Previous match";
15
17
  readonly rotate: "Rotate";
16
18
  readonly search: "Search in file";
19
+ readonly sheetTruncated: "Showing the first {count} rows";
17
20
  readonly unsupported: "This file type cannot be previewed here. Download it to open it.";
18
21
  readonly zoomIn: "Zoom in";
19
22
  readonly zoomOut: "Zoom out";
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsFileViewerLabels = void 0;
4
4
  exports.PsFileViewerLabels = {
5
5
  close: 'Close',
6
+ closeSearch: 'Close search',
6
7
  delete: 'Delete',
7
8
  deleteConfirm: 'Delete file',
8
9
  deleteConfirmBody: 'will be deleted. You cannot undo this.',
9
10
  deleteCancel: 'Cancel',
10
11
  download: 'Download',
11
12
  loadFailed: 'This file could not be opened.',
13
+ matchCount: '{current} of {total}',
12
14
  next: 'Next file',
13
15
  nextMatch: 'Next match',
14
16
  noMatches: 'No matches',
@@ -17,6 +19,7 @@ exports.PsFileViewerLabels = {
17
19
  previousMatch: 'Previous match',
18
20
  rotate: 'Rotate',
19
21
  search: 'Search in file',
22
+ sheetTruncated: 'Showing the first {count} rows',
20
23
  unsupported: 'This file type cannot be previewed here. Download it to open it.',
21
24
  zoomIn: 'Zoom in',
22
25
  zoomOut: 'Zoom out',
@@ -0,0 +1,8 @@
1
+ export declare const PsHtmlSanitizeConfig: {
2
+ readonly anchorRel: "noopener noreferrer";
3
+ readonly anchorTarget: "_blank";
4
+ readonly blockSelector: "p,li,td,th,tr,h1,h2,h3,h4,h5,h6,div,pre,blockquote,section,article,caption";
5
+ readonly forbiddenAttributes: readonly ["style", "srcset"];
6
+ readonly forbiddenTags: readonly ["style", "form", "input", "button", "textarea", "select", "iframe", "frame", "object", "embed", "link", "meta", "base", "script"];
7
+ readonly mediaTags: readonly ["img", "picture", "source", "video", "audio", "track"];
8
+ };
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsHtmlSanitizeConfig = void 0;
4
+ exports.PsHtmlSanitizeConfig = {
5
+ anchorRel: 'noopener noreferrer',
6
+ anchorTarget: '_blank',
7
+ blockSelector: 'p,li,td,th,tr,h1,h2,h3,h4,h5,h6,div,pre,blockquote,section,article,caption',
8
+ forbiddenAttributes: ['style', 'srcset'],
9
+ forbiddenTags: [
10
+ 'style',
11
+ 'form',
12
+ 'input',
13
+ 'button',
14
+ 'textarea',
15
+ 'select',
16
+ 'iframe',
17
+ 'frame',
18
+ 'object',
19
+ 'embed',
20
+ 'link',
21
+ 'meta',
22
+ 'base',
23
+ 'script',
24
+ ],
25
+ mediaTags: ['img', 'picture', 'source', 'video', 'audio', 'track'],
26
+ };
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsLabelService } from '../services/ps-label-service';
5
+ import { PsFileViewerButton } from './ps-file-viewer-button';
6
+ export const PsFileSearchBar = ({ className, labels, onClose, state }) => (_jsxs("div", { className: PallasoftClassNameService.merge('flex items-center gap-1 rounded-ps-control bg-white/10 px-3', className), role: 'search', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4 shrink-0 text-white/60', iconKey: PsIconKey.Search }), _jsx("input", { "aria-label": labels.search, className: 'h-ps-control-compact min-w-0 flex-1 bg-transparent text-sm text-white placeholder:text-white/45 focus:outline-none', onChange: (event) => state.setSearch(event.target.value), onKeyDown: (event) => {
7
+ if (event.key === 'Enter') {
8
+ event.preventDefault();
9
+ state.step(event.shiftKey ? -1 : 1);
10
+ }
11
+ else if (event.key === 'Escape' && onClose !== undefined) {
12
+ event.preventDefault();
13
+ event.stopPropagation();
14
+ onClose();
15
+ }
16
+ }, placeholder: labels.search, ref: state.inputRef, type: 'search', value: state.search }), state.query.trim().length > 0 ? (_jsx("span", { "aria-live": 'polite', className: 'shrink-0 whitespace-nowrap text-xs text-white/60', children: state.matchCount === 0
17
+ ? labels.noMatches
18
+ : PsLabelService.fill(labels.matchCount, {
19
+ current: state.activeMatch + 1,
20
+ total: state.matchCount,
21
+ }) })) : null, _jsx(PsFileViewerButton, { disabled: state.matchCount === 0, label: labels.previousMatch, onClick: () => state.step(-1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone -rotate-90', iconKey: PsIconKey.ChevronRight }) }), _jsx(PsFileViewerButton, { disabled: state.matchCount === 0, label: labels.nextMatch, onClick: () => state.step(1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone rotate-90', iconKey: PsIconKey.ChevronRight }) }), onClose === undefined ? null : (_jsx(PsFileViewerButton, { label: labels.closeSearch, onClick: onClose, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Close }) }))] }));
@@ -1,11 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { PsFileKind } from '../constants/ps-file-kind';
3
3
  import { PsFileTypeService } from '../services/ps-file-type-service';
4
+ import { PsHtmlViewer } from './ps-html-viewer';
4
5
  import { PsImageViewer } from './ps-image-viewer';
5
6
  import { PsMediaViewer } from './ps-media-viewer';
6
7
  import { PsPdfViewer } from './ps-pdf-viewer';
8
+ import { PsSheetViewer } from './ps-sheet-viewer';
7
9
  import { PsTextViewer } from './ps-text-viewer';
8
10
  import { PsUnsupportedViewer } from './ps-unsupported-viewer';
11
+ import { PsWordViewer } from './ps-word-viewer';
9
12
  export const PsFileViewerContent = (props) => {
10
13
  switch (PsFileTypeService.kindOf(props.file.contentType, props.file.name)) {
11
14
  case PsFileKind.image:
@@ -15,6 +18,12 @@ export const PsFileViewerContent = (props) => {
15
18
  case PsFileKind.csv:
16
19
  case PsFileKind.text:
17
20
  return _jsx(PsTextViewer, { ...props });
21
+ case PsFileKind.word:
22
+ return _jsx(PsWordViewer, { ...props });
23
+ case PsFileKind.sheet:
24
+ return _jsx(PsSheetViewer, { ...props });
25
+ case PsFileKind.html:
26
+ return _jsx(PsHtmlViewer, { ...props });
18
27
  case PsFileKind.audio:
19
28
  case PsFileKind.video:
20
29
  return _jsx(PsMediaViewer, { ...props });
@@ -1,22 +1,46 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useCallback, useRef, useState } from 'react';
3
3
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
4
  import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
5
5
  import { PsFileViewerKinds } from '../constants/ps-file-viewer-kinds';
6
6
  import { PsFileViewerLabels } from '../constants/ps-file-viewer-labels';
7
+ import { usePsFileSearch } from '../hooks/use-ps-file-search';
8
+ import { usePsFindShortcut } from '../hooks/use-ps-find-shortcut';
9
+ import { PsFileSearchService } from '../services/ps-file-search-service';
7
10
  import { PsFileTypeService } from '../services/ps-file-type-service';
11
+ import { PsFileSearchBar } from './ps-file-search-bar';
8
12
  import { PsFileViewerButton } from './ps-file-viewer-button';
9
13
  import { PsFileViewerContent } from './ps-file-viewer-content';
10
- export const PsFileViewerPane = ({ contentType, headerActions, id, labels = PsFileViewerLabels, name, pdfWorkerSrc, url, }) => {
14
+ export const PsFileViewerPane = ({ contentType, headerActions, id, labels, name, pdfWorkerSrc, textLayer, url, }) => {
15
+ const text = { ...PsFileViewerLabels, ...labels };
11
16
  const [zoom, setZoom] = useState(1);
12
17
  const [rotation, setRotation] = useState(0);
18
+ const [isFindOpen, setIsFindOpen] = useState(false);
19
+ const root = useRef(null);
13
20
  const kind = PsFileTypeService.kindOf(contentType, name);
14
21
  const canZoom = PsFileViewerKinds.zoomable.includes(kind);
15
22
  const canRotate = PsFileViewerKinds.rotatable.includes(kind);
23
+ const canSearch = PsFileSearchService.canSearch(kind, textLayer !== undefined);
16
24
  const changeZoom = (step) => setZoom((current) => Math.min(PsFileViewerConfig.maxZoom, Math.max(PsFileViewerConfig.minZoom, current + step)));
17
25
  const fileKey = id === undefined ? url : id;
26
+ const searchState = usePsFileSearch(fileKey);
27
+ const { focus, reset } = searchState;
28
+ const openFind = useCallback(() => {
29
+ setIsFindOpen(true);
30
+ requestAnimationFrame(focus);
31
+ }, [focus]);
32
+ const closeFind = useCallback(() => {
33
+ setIsFindOpen(false);
34
+ reset();
35
+ }, [reset]);
36
+ usePsFindShortcut(root, canSearch, openFind);
18
37
  const rotate = () => setRotation((current) => (current + PsFileViewerConfig.rotateStep) % 360);
19
38
  const onKeyDown = (event) => {
39
+ if (event.key === 'Escape' && isFindOpen) {
40
+ closeFind();
41
+ event.preventDefault();
42
+ return;
43
+ }
20
44
  if (event.metaKey || event.ctrlKey || event.altKey)
21
45
  return;
22
46
  if (canZoom && (event.key === '+' || event.key === '='))
@@ -31,5 +55,5 @@ export const PsFileViewerPane = ({ contentType, headerActions, id, labels = PsFi
31
55
  return;
32
56
  event.preventDefault();
33
57
  };
34
- return (_jsxs("div", { className: 'flex h-full min-h-0 min-w-0 flex-col overflow-hidden rounded-ps-panel bg-black/85', children: [_jsxs("div", { className: 'flex shrink-0 flex-wrap items-center justify-between gap-x-2 border-b border-white/10 px-2 py-1 text-white', children: [_jsx("p", { className: 'min-w-0 flex-1 basis-32 truncate px-1 text-xs text-white/70', title: name, children: name }), _jsxs("div", { className: 'flex items-center gap-0.5', role: 'toolbar', children: [canZoom ? (_jsxs(_Fragment, { children: [_jsx(PsFileViewerButton, { disabled: zoom <= PsFileViewerConfig.minZoom, label: labels.zoomOut, onClick: () => changeZoom(-PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.ZoomOut }) }), _jsxs(PsFileViewerButton, { className: 'w-14 text-xs', label: labels.zoomReset, onClick: () => setZoom(1), children: [Math.round(zoom * 100), "%"] }), _jsx(PsFileViewerButton, { disabled: zoom >= PsFileViewerConfig.maxZoom, label: labels.zoomIn, onClick: () => changeZoom(PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.ZoomIn }) })] })) : null, canRotate ? (_jsx(PsFileViewerButton, { label: labels.rotate, onClick: rotate, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Rotate }) })) : null, headerActions] })] }), _jsx("div", { className: 'ps-focus-ring flex min-h-0 flex-1 p-2', onKeyDown: onKeyDown, tabIndex: 0, children: _jsx(PsFileViewerContent, { activeMatch: 0, file: { contentType, id: fileKey, name, url }, labels: labels, onMatchCount: () => undefined, pdfWorkerSrc: pdfWorkerSrc, query: '', rotation: rotation, zoom: zoom }, fileKey) })] }));
58
+ return (_jsxs("div", { className: 'flex h-full min-h-0 min-w-0 flex-col overflow-hidden rounded-ps-panel bg-black/85', ref: root, children: [_jsxs("div", { className: 'flex shrink-0 flex-wrap items-center justify-between gap-x-2 border-b border-white/10 px-2 py-1 text-white', children: [_jsx("p", { className: 'min-w-0 flex-1 basis-32 truncate px-1 text-xs text-white/70', title: name, children: name }), _jsxs("div", { className: 'flex items-center gap-0.5', role: 'toolbar', children: [canSearch ? (_jsx(PsFileViewerButton, { label: text.search, onClick: isFindOpen ? closeFind : openFind, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Search }) })) : null, canZoom ? (_jsxs(_Fragment, { children: [_jsx(PsFileViewerButton, { disabled: zoom <= PsFileViewerConfig.minZoom, label: text.zoomOut, onClick: () => changeZoom(-PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.ZoomOut }) }), _jsxs(PsFileViewerButton, { className: 'w-14 text-xs', label: text.zoomReset, onClick: () => setZoom(1), children: [Math.round(zoom * 100), "%"] }), _jsx(PsFileViewerButton, { disabled: zoom >= PsFileViewerConfig.maxZoom, label: text.zoomIn, onClick: () => changeZoom(PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.ZoomIn }) })] })) : null, canRotate ? (_jsx(PsFileViewerButton, { label: text.rotate, onClick: rotate, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Rotate }) })) : null, headerActions] })] }), canSearch && isFindOpen ? (_jsx("div", { className: 'shrink-0 border-b border-white/10 px-2 py-1', children: _jsx(PsFileSearchBar, { labels: text, onClose: closeFind, state: searchState }) })) : null, _jsx("div", { className: 'ps-focus-ring flex min-h-0 flex-1 p-2', onKeyDown: onKeyDown, tabIndex: 0, children: _jsx(PsFileViewerContent, { activeMatch: searchState.activeMatch, file: { contentType, id: fileKey, name, textLayer, url }, labels: text, onMatchCount: searchState.setMatchCount, pdfWorkerSrc: pdfWorkerSrc, query: canSearch && isFindOpen ? searchState.query : '', rotation: rotation, zoom: zoom }, fileKey) })] }));
35
59
  };
@@ -1,15 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
3
- import { PsLayer, usePsDebouncedValue, usePsExitTransition, usePsLayerZIndex, } from '@pallasoft/ps-core-components';
2
+ import { useEffect, useState } from 'react';
3
+ import { PsLayer, usePsExitTransition, usePsLayerZIndex } from '@pallasoft/ps-core-components';
4
4
  import { PallasoftClassNameService, PallasoftZIndex } from '@pallasoft/ps-design-system';
5
5
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
6
6
  import { createPortal } from 'react-dom';
7
7
  import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
8
8
  import { PsFileViewerKinds } from '../constants/ps-file-viewer-kinds';
9
9
  import { PsFileViewerLabels } from '../constants/ps-file-viewer-labels';
10
+ import { usePsFileSearch } from '../hooks/use-ps-file-search';
11
+ import { usePsFindShortcut } from '../hooks/use-ps-find-shortcut';
12
+ import { PsFileSearchService } from '../services/ps-file-search-service';
10
13
  import { PsFileSizeService } from '../services/ps-file-size-service';
11
14
  import { PsFileTypeService } from '../services/ps-file-type-service';
12
15
  import { PsFileDeleteConfirm } from './ps-file-delete-confirm';
16
+ import { PsFileSearchBar } from './ps-file-search-bar';
13
17
  import { PsFileViewerButton } from './ps-file-viewer-button';
14
18
  import { PsFileViewerContent } from './ps-file-viewer-content';
15
19
  export const PsFileViewer = ({ files, index, isOpen, labels, onClose, onDelete, onIndexChange, pdfWorkerSrc, }) => {
@@ -18,34 +22,24 @@ export const PsFileViewer = ({ files, index, isOpen, labels, onClose, onDelete,
18
22
  const zIndex = usePsLayerZIndex(PallasoftZIndex.Modal);
19
23
  const [zoom, setZoom] = useState(1);
20
24
  const [rotation, setRotation] = useState(0);
21
- const [search, setSearch] = useState('');
22
- const [activeMatch, setActiveMatch] = useState(0);
23
- const [matchCount, setMatchCount] = useState(0);
24
25
  const [isConfirming, setIsConfirming] = useState(false);
25
26
  const [isDeleting, setIsDeleting] = useState(false);
26
- const searchInput = useRef(null);
27
- const query = usePsDebouncedValue(search, PsFileViewerConfig.searchDebounceMs);
28
27
  const file = files.length === 0 ? null : files[Math.min(index, files.length - 1)];
29
28
  const kind = file === null ? null : PsFileTypeService.kindOf(file.contentType, file.name);
30
- const canSearch = kind !== null && PsFileViewerKinds.searchable.includes(kind);
29
+ const searchState = usePsFileSearch(file === null ? null : file.id);
30
+ const canSearch = kind !== null &&
31
+ file !== null &&
32
+ PsFileSearchService.canSearch(kind, file.textLayer !== undefined);
31
33
  const canZoom = kind !== null && PsFileViewerKinds.zoomable.includes(kind);
32
34
  const canRotate = kind !== null && PsFileViewerKinds.rotatable.includes(kind);
33
35
  useEffect(() => {
34
36
  setZoom(1);
35
37
  setRotation(0);
36
- setSearch('');
37
- setActiveMatch(0);
38
- setMatchCount(0);
39
38
  }, [file === null ? null : file.id]);
40
- useEffect(() => setActiveMatch(0), [query]);
41
39
  const go = (step) => {
42
40
  if (files.length > 1)
43
41
  onIndexChange((index + step + files.length) % files.length);
44
42
  };
45
- const stepMatch = (step) => {
46
- if (matchCount > 0)
47
- setActiveMatch((current) => (current + step + matchCount) % matchCount);
48
- };
49
43
  const changeZoom = (step) => setZoom((current) => Math.min(PsFileViewerConfig.maxZoom, Math.max(PsFileViewerConfig.minZoom, current + step)));
50
44
  useEffect(() => {
51
45
  if (!isOpen)
@@ -54,10 +48,6 @@ export const PsFileViewer = ({ files, index, isOpen, labels, onClose, onDelete,
54
48
  const isTyping = event.target instanceof HTMLInputElement;
55
49
  if (event.key === 'Escape' && !isConfirming)
56
50
  onClose();
57
- else if ((event.metaKey || event.ctrlKey) && event.key === 'f' && canSearch) {
58
- event.preventDefault();
59
- searchInput.current?.focus();
60
- }
61
51
  else if (isTyping)
62
52
  return;
63
53
  else if (event.key === 'ArrowRight')
@@ -71,7 +61,8 @@ export const PsFileViewer = ({ files, index, isOpen, labels, onClose, onDelete,
71
61
  };
72
62
  window.addEventListener('keydown', onKey);
73
63
  return () => window.removeEventListener('keydown', onKey);
74
- }, [isOpen, isConfirming, canSearch, index, files.length]);
64
+ }, [isOpen, isConfirming, index, files.length]);
65
+ usePsFindShortcut(null, isOpen && canSearch, searchState.focus);
75
66
  if (!shouldRender || file === null || typeof document === 'undefined')
76
67
  return null;
77
68
  const confirmDelete = async () => {
@@ -91,8 +82,5 @@ export const PsFileViewer = ({ files, index, isOpen, labels, onClose, onDelete,
91
82
  file.sizeBytes === undefined ? null : PsFileSizeService.format(file.sizeBytes),
92
83
  ]
93
84
  .filter((value) => value !== null)
94
- .join(' · ') })] }) }), canSearch ? (_jsxs("div", { className: 'order-last flex w-full items-center gap-1 rounded-ps-control bg-white/10 px-3 sm:order-none sm:w-72', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4 shrink-0 text-white/60', iconKey: PsIconKey.Search }), _jsx("input", { "aria-label": text.search, className: 'h-ps-control-compact min-w-0 flex-1 bg-transparent text-sm text-white placeholder:text-white/45 focus:outline-none', onChange: (event) => setSearch(event.target.value), onKeyDown: (event) => {
95
- if (event.key === 'Enter')
96
- stepMatch(event.shiftKey ? -1 : 1);
97
- }, placeholder: text.search, ref: searchInput, type: 'search', value: search }), query.trim().length > 0 ? (_jsx("span", { className: 'shrink-0 text-xs text-white/60', children: matchCount === 0 ? text.noMatches : `${activeMatch + 1} / ${matchCount}` })) : null, _jsx(PsFileViewerButton, { disabled: matchCount === 0, label: text.previousMatch, onClick: () => stepMatch(-1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone -rotate-90', iconKey: PsIconKey.ChevronRight }) }), _jsx(PsFileViewerButton, { disabled: matchCount === 0, label: text.nextMatch, onClick: () => stepMatch(1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone rotate-90', iconKey: PsIconKey.ChevronRight }) })] })) : null, _jsxs("div", { className: 'flex items-center gap-0.5', children: [canZoom ? (_jsxs(_Fragment, { children: [_jsx(PsFileViewerButton, { label: text.zoomOut, onClick: () => changeZoom(-PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.ZoomOut }) }), _jsxs(PsFileViewerButton, { className: 'w-14 text-xs', label: text.zoomReset, onClick: () => setZoom(1), children: [Math.round(zoom * 100), "%"] }), _jsx(PsFileViewerButton, { label: text.zoomIn, onClick: () => changeZoom(PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.ZoomIn }) })] })) : null, canRotate ? (_jsx(PsFileViewerButton, { label: text.rotate, onClick: () => setRotation((current) => (current + PsFileViewerConfig.rotateStep) % 360), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Rotate }) })) : null, _jsx("a", { "aria-label": text.download, className: 'ps-focus-ring ps-control-square ps-control-compact inline-flex items-center justify-center rounded-ps-control text-white/80 transition hover:bg-white/15 hover:text-white', download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, title: text.download, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Download }) }), onDelete ? (_jsx(PsFileViewerButton, { label: text.delete, onClick: () => setIsConfirming(true), tone: 'danger', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Trash }) })) : null, _jsx(PsFileViewerButton, { label: text.close, onClick: onClose, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Close }) })] })] }), _jsxs("div", { className: 'relative flex min-h-0 flex-1', children: [files.length > 1 ? (_jsx(PsFileViewerButton, { className: 'absolute left-2 top-1/2 z-docked size-ps-control -translate-y-1/2 bg-black/40', label: text.previous, onClick: () => go(-1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone rotate-180', iconKey: PsIconKey.ChevronRight }) })) : null, _jsx("div", { className: 'flex min-h-0 min-w-0 flex-1 p-3 sm:p-6', children: _jsx(PsFileViewerContent, { activeMatch: activeMatch, file: file, labels: text, onMatchCount: setMatchCount, pdfWorkerSrc: pdfWorkerSrc, query: canSearch ? query : '', rotation: rotation, zoom: zoom }, file.id) }), files.length > 1 ? (_jsx(PsFileViewerButton, { className: 'absolute right-2 top-1/2 z-docked size-ps-control -translate-y-1/2 bg-black/40', label: text.next, onClick: () => go(1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.ChevronRight }) })) : null] }), _jsx(PsFileDeleteConfirm, { fileName: file.name, isDeleting: isDeleting, isOpen: isConfirming, labels: text, onCancel: () => setIsConfirming(false), onConfirm: () => void confirmDelete() })] }) }), document.body);
85
+ .join(' · ') })] }) }), canSearch ? (_jsx(PsFileSearchBar, { className: 'order-last w-full sm:order-none sm:w-72', labels: text, state: searchState })) : null, _jsxs("div", { className: 'flex items-center gap-0.5', children: [canZoom ? (_jsxs(_Fragment, { children: [_jsx(PsFileViewerButton, { label: text.zoomOut, onClick: () => changeZoom(-PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.ZoomOut }) }), _jsxs(PsFileViewerButton, { className: 'w-14 text-xs', label: text.zoomReset, onClick: () => setZoom(1), children: [Math.round(zoom * 100), "%"] }), _jsx(PsFileViewerButton, { label: text.zoomIn, onClick: () => changeZoom(PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.ZoomIn }) })] })) : null, canRotate ? (_jsx(PsFileViewerButton, { label: text.rotate, onClick: () => setRotation((current) => (current + PsFileViewerConfig.rotateStep) % 360), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Rotate }) })) : null, _jsx("a", { "aria-label": text.download, className: 'ps-focus-ring ps-control-square ps-control-compact inline-flex items-center justify-center rounded-ps-control text-white/80 transition hover:bg-white/15 hover:text-white', download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, title: text.download, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Download }) }), onDelete ? (_jsx(PsFileViewerButton, { label: text.delete, onClick: () => setIsConfirming(true), tone: 'danger', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Trash }) })) : null, _jsx(PsFileViewerButton, { label: text.close, onClick: onClose, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.Close }) })] })] }), _jsxs("div", { className: 'relative flex min-h-0 flex-1', children: [files.length > 1 ? (_jsx(PsFileViewerButton, { className: 'absolute left-2 top-1/2 z-docked size-ps-control -translate-y-1/2 bg-black/40', label: text.previous, onClick: () => go(-1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone rotate-180', iconKey: PsIconKey.ChevronRight }) })) : null, _jsx("div", { className: 'flex min-h-0 min-w-0 flex-1 p-3 sm:p-6', children: _jsx(PsFileViewerContent, { activeMatch: searchState.activeMatch, file: file, labels: text, onMatchCount: searchState.setMatchCount, pdfWorkerSrc: pdfWorkerSrc, query: canSearch ? searchState.query : '', rotation: rotation, zoom: zoom }, file.id) }), files.length > 1 ? (_jsx(PsFileViewerButton, { className: 'absolute right-2 top-1/2 z-docked size-ps-control -translate-y-1/2 bg-black/40', label: text.next, onClick: () => go(1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-ps-control-icon-standalone', iconKey: PsIconKey.ChevronRight }) })) : null] }), _jsx(PsFileDeleteConfirm, { fileName: file.name, isDeleting: isDeleting, isOpen: isConfirming, labels: text, onCancel: () => setIsConfirming(false), onConfirm: () => void confirmDelete() })] }) }), document.body);
98
86
  };
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from 'react';
3
+ import { usePsActiveMatchScroll } from '../hooks/use-ps-active-match-scroll';
4
+ import { PsHtmlService } from '../services/ps-html-service';
5
+ export const PsHtmlDocument = ({ activeMatch, html, onMatchCount, query, zoom, }) => {
6
+ const container = useRef(null);
7
+ const highlighted = useMemo(() => PsHtmlService.highlight(html, query), [html, query]);
8
+ useEffect(() => onMatchCount(highlighted.count), [highlighted.count]);
9
+ useEffect(() => {
10
+ container.current?.querySelectorAll('mark[data-ps-match]').forEach((mark) => {
11
+ if (mark.getAttribute('data-ps-match') === String(activeMatch))
12
+ mark.setAttribute('data-ps-active', '');
13
+ else
14
+ mark.removeAttribute('data-ps-active');
15
+ });
16
+ }, [highlighted.html, activeMatch]);
17
+ usePsActiveMatchScroll(container, activeMatch, highlighted.html);
18
+ return (_jsx("div", { className: 'h-full w-full overflow-auto', children: _jsx("div", { className: 'ps-html-document mx-auto w-full max-w-4xl break-words rounded-lg bg-surface p-8 leading-relaxed text-ink shadow-2xl [&_a]:text-primary [&_a]:underline [&_blockquote]:border-l-4 [&_blockquote]:border-line [&_blockquote]:pl-4 [&_h1]:mb-3 [&_h1]:text-2xl [&_h1]:font-bold [&_h2]:mb-2 [&_h2]:text-xl [&_h2]:font-bold [&_h3]:mb-2 [&_h3]:text-lg [&_h3]:font-semibold [&_img]:h-auto [&_img]:max-w-full [&_li]:my-1 [&_mark]:rounded-sm [&_mark]:bg-warning/40 [&_mark]:text-inherit [&_mark[data-ps-active]]:bg-warning [&_mark[data-ps-active]]:text-ink [&_mark[data-ps-active]]:ring-2 [&_mark[data-ps-active]]:ring-warning [&_ol]:list-decimal [&_ol]:pl-6 [&_p]:mb-3 [&_table]:my-3 [&_table]:border-collapse [&_td]:border [&_td]:border-line [&_td]:px-2 [&_td]:py-1 [&_th]:border [&_th]:border-line [&_th]:px-2 [&_th]:py-1 [&_ul]:list-disc [&_ul]:pl-6', dangerouslySetInnerHTML: { __html: highlighted.html }, ref: container, style: { fontSize: `${zoom}rem` } }) }));
19
+ };
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PsSpinner } from '@pallasoft/ps-core-components';
3
+ import { usePsLoadedFile } from '../hooks/use-ps-loaded-file';
4
+ import { PsHtmlFileService } from '../services/ps-html-file-service';
5
+ import { PsHtmlDocument } from './ps-html-document';
6
+ export const PsHtmlViewer = ({ activeMatch, file, labels, onMatchCount, query, zoom, }) => {
7
+ const { hasFailed, value } = usePsLoadedFile(file.url, PsHtmlFileService.load);
8
+ if (hasFailed)
9
+ return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
10
+ if (value === null)
11
+ return _jsx(PsSpinner, { className: 'm-auto', color: 'current', size: 'large' });
12
+ return (_jsx(PsHtmlDocument, { activeMatch: activeMatch, html: value, onMatchCount: onMatchCount, query: query, zoom: zoom }));
13
+ };
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ export const PsImageTextHighlights = ({ activeIndex, image, matches, transform, }) => {
4
+ const [size, setSize] = useState({ height: 0, width: 0 });
5
+ useEffect(() => {
6
+ const node = image.current;
7
+ if (node === null)
8
+ return;
9
+ const measure = () => setSize({ height: node.clientHeight, width: node.clientWidth });
10
+ measure();
11
+ if (typeof ResizeObserver === 'undefined') {
12
+ window.addEventListener('resize', measure);
13
+ return () => window.removeEventListener('resize', measure);
14
+ }
15
+ const observer = new ResizeObserver(measure);
16
+ observer.observe(node);
17
+ return () => observer.disconnect();
18
+ }, [image]);
19
+ return (_jsx("div", { className: 'pointer-events-none absolute inset-0 flex items-center justify-center', children: _jsx("div", { className: 'relative shrink-0 transition-transform duration-150', style: { height: size.height, transform, width: size.width }, children: matches.map((match, index) => (_jsx("span", { className: match.matchIndex === activeIndex
20
+ ? 'absolute rounded-sm bg-warning/60 ring-2 ring-warning'
21
+ : 'absolute rounded-sm bg-warning/35', style: {
22
+ height: `${match.height * 100}%`,
23
+ left: `${match.left * 100}%`,
24
+ top: `${match.top * 100}%`,
25
+ width: `${match.width * 100}%`,
26
+ } }, index))) }) }));
27
+ };
@@ -1,14 +1,23 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { usePsDragPan } from '../hooks/use-ps-drag-pan';
4
+ import { PsImageTextService } from '../services/ps-image-text-service';
4
5
  import { PsPanService } from '../services/ps-pan-service';
5
- export const PsImageViewer = ({ file, labels, rotation, zoom }) => {
6
+ import { PsImageTextHighlights } from './ps-image-text-highlights';
7
+ export const PsImageViewer = ({ activeMatch, file, labels, onMatchCount, query, rotation, zoom, }) => {
6
8
  const [offset, setOffset] = useState({ x: 0, y: 0 });
7
9
  const [hasFailed, setHasFailed] = useState(false);
8
10
  const pan = useRef(null);
9
11
  const base = useRef({ x: 0, y: 0 });
10
12
  const frame = useRef(0);
11
13
  const pending = useRef({ x: 0, y: 0 });
14
+ const image = useRef(null);
15
+ const layer = file.textLayer;
16
+ const matches = useMemo(() => (layer === undefined ? [] : PsImageTextService.matches(layer, query)), [layer, query]);
17
+ const count = matches.length === 0 ? 0 : matches[matches.length - 1].matchIndex + 1;
18
+ const activeIndex = count === 0 ? -1 : activeMatch % count;
19
+ const imageTransform = `scale(${zoom}) rotate(${rotation}deg)`;
20
+ useEffect(() => onMatchCount(count), [count]);
12
21
  useEffect(() => {
13
22
  setOffset({ x: 0, y: 0 });
14
23
  setHasFailed(false);
@@ -44,5 +53,5 @@ export const PsImageViewer = ({ file, labels, rotation, zoom }) => {
44
53
  });
45
54
  if (hasFailed)
46
55
  return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
47
- return (_jsx("div", { className: `h-full w-full touch-none overflow-hidden ${zoom > 1 ? 'cursor-grab active:cursor-grabbing' : ''}`, ...handlers, children: _jsx("div", { className: 'flex h-full w-full items-center justify-center', ref: pan, style: { transform: PsPanService.translate(offset) }, children: _jsx("img", { alt: file.name, className: 'max-h-full max-w-full select-none object-contain shadow-2xl transition-transform duration-150', draggable: false, onError: () => setHasFailed(true), src: file.url, style: { transform: `scale(${zoom}) rotate(${rotation}deg)` } }) }) }));
56
+ return (_jsx("div", { className: `h-full w-full touch-none overflow-hidden ${zoom > 1 ? 'cursor-grab active:cursor-grabbing' : ''}`, ...handlers, children: _jsxs("div", { className: 'relative flex h-full w-full items-center justify-center', ref: pan, style: { transform: PsPanService.translate(offset) }, children: [_jsx("img", { alt: file.name, className: 'max-h-full max-w-full select-none object-contain shadow-2xl transition-transform duration-150', draggable: false, onError: () => setHasFailed(true), ref: image, src: file.url, style: { transform: imageTransform } }), matches.length > 0 ? (_jsx(PsImageTextHighlights, { activeIndex: activeIndex, image: image, matches: matches, transform: imageTransform })) : null] }) }));
48
57
  };
@@ -22,7 +22,7 @@ export const PsPdfPage = ({ activeMatch, document, label, matches, pageNumber, r
22
22
  task?.cancel();
23
23
  };
24
24
  }, [document, pageNumber, rotation, scale, isInView]);
25
- return (_jsxs("div", { "aria-label": `${label} ${pageNumber}`, className: 'relative mx-auto bg-white shadow-2xl', "data-ps-pdf-page": pageNumber, ref: container, style: size === null ? { height: 800, width: 600 } : { height: size.height, width: size.width }, children: [_jsx("canvas", { className: 'block', ref: canvas }), matches.map((match, index) => (_jsx("span", { className: match === activeMatch
25
+ return (_jsxs("div", { "aria-label": `${label} ${pageNumber}`, className: 'relative mx-auto bg-white shadow-2xl', "data-ps-pdf-page": pageNumber, ref: container, style: size === null ? { height: 800, width: 600 } : { height: size.height, width: size.width }, children: [_jsx("canvas", { className: 'block', ref: canvas }), matches.map((match, index) => (_jsx("span", { className: activeMatch !== null && match.matchIndex === activeMatch.matchIndex
26
26
  ? 'pointer-events-none absolute rounded-sm bg-warning/60 ring-2 ring-warning'
27
27
  : 'pointer-events-none absolute rounded-sm bg-warning/35', style: {
28
28
  height: `${match.height * 100}%`,
@@ -76,13 +76,17 @@ export const PsPdfViewer = ({ activeMatch, file, labels, onMatchCount, pdfWorker
76
76
  if (!isCurrent)
77
77
  return;
78
78
  setMatches(found);
79
- onMatchCount(found.length);
79
+ onMatchCount(found.length === 0 ? 0 : found[found.length - 1].matchIndex + 1);
80
80
  });
81
81
  return () => {
82
82
  isCurrent = false;
83
83
  };
84
84
  }, [document, query, rotation]);
85
- const active = matches.length > 0 ? matches[activeMatch % matches.length] : null;
85
+ const matchTotal = matches.length === 0 ? 0 : matches[matches.length - 1].matchIndex + 1;
86
+ const activeRect = matchTotal === 0
87
+ ? undefined
88
+ : matches.find((match) => match.matchIndex === activeMatch % matchTotal);
89
+ const active = activeRect === undefined ? null : activeRect;
86
90
  const scrollToPage = (pageNumber) => {
87
91
  const node = scroller.current?.querySelector(`[data-ps-pdf-page='${pageNumber}']`);
88
92
  if (node instanceof HTMLElement)
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { PsSpinner } from '@pallasoft/ps-core-components';
4
+ import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
5
+ import { usePsLoadedFile } from '../hooks/use-ps-loaded-file';
6
+ import { PsLabelService } from '../services/ps-label-service';
7
+ import { PsSheetService } from '../services/ps-sheet-service';
8
+ import { PsTableView } from './ps-table-view';
9
+ export const PsSheetViewer = ({ activeMatch, file, labels, onMatchCount, query, zoom, }) => {
10
+ const { hasFailed, value } = usePsLoadedFile(file.url, PsSheetService.load);
11
+ const [sheetIndex, setSheetIndex] = useState(0);
12
+ const sheet = value === null || value.length === 0 ? null : value[Math.min(sheetIndex, value.length - 1)];
13
+ const rows = useMemo(() => (sheet === null ? [] : sheet.rows.slice(0, PsFileViewerConfig.maxSheetRows)), [sheet]);
14
+ useEffect(() => setSheetIndex(0), [file.id]);
15
+ if (hasFailed || (value !== null && value.length === 0))
16
+ return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
17
+ if (value === null || sheet === null)
18
+ return _jsx(PsSpinner, { className: 'm-auto', color: 'current', size: 'large' });
19
+ return (_jsxs("div", { className: 'flex h-full w-full min-w-0 flex-col gap-2', children: [_jsx("div", { className: 'min-h-0 flex-1 overflow-auto', children: _jsx("div", { className: 'mx-auto w-full max-w-6xl rounded-lg bg-surface p-5 text-ink shadow-2xl', style: { fontSize: `${zoom * 0.875}rem` }, children: _jsx(PsTableView, { activeMatch: activeMatch, onMatchCount: onMatchCount, query: query, rows: rows }, sheet.name) }) }), _jsxs("div", { className: 'flex shrink-0 flex-wrap items-center gap-2 text-xs text-white/70', children: [value.length > 1 ? (_jsx("div", { className: 'flex flex-wrap gap-1', role: 'tablist', children: value.map((entry, index) => (_jsx("button", { "aria-selected": entry === sheet, className: entry === sheet
20
+ ? 'ps-focus-ring ps-control-compact rounded-ps-control bg-white/20 px-3 text-white'
21
+ : 'ps-focus-ring ps-control-compact rounded-ps-control px-3 text-white/70 hover:bg-white/10', onClick: () => setSheetIndex(index), role: 'tab', type: 'button', children: entry.name }, `${entry.name}-${index}`))) })) : null, sheet.rows.length > rows.length ? (_jsx("span", { children: PsLabelService.fill(labels.sheetTruncated, { count: PsFileViewerConfig.maxSheetRows }) })) : null] })] }));
22
+ };
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from 'react';
3
+ import { usePsActiveMatchScroll } from '../hooks/use-ps-active-match-scroll';
4
+ import { PsTextSearchService } from '../services/ps-text-search-service';
5
+ import { PsHighlightedText } from './ps-highlighted-text';
6
+ export const PsTableView = ({ activeMatch, onMatchCount, query, rows }) => {
7
+ const container = useRef(null);
8
+ const cells = useMemo(() => PsTextSearchService.tableSegments(rows, query), [rows, query]);
9
+ const count = useMemo(() => PsTextSearchService.countSegments(cells), [cells]);
10
+ useEffect(() => onMatchCount(count), [count]);
11
+ usePsActiveMatchScroll(container, activeMatch, query);
12
+ return (_jsx("table", { className: 'w-full border-collapse text-left', ref: container, children: _jsx("tbody", { children: cells.map((row, rowIndex) => (_jsx("tr", { className: rowIndex === 0 ? 'bg-primary-soft/50 font-semibold' : 'border-t border-line', children: row.map((cell, cellIndex) => (_jsx("td", { className: 'whitespace-nowrap px-3 py-1.5 align-top', children: _jsx(PsHighlightedText, { activeMatch: activeMatch, segments: cell }) }, cellIndex))) }, rowIndex))) }) }));
13
+ };