@pallasoft/ps-file-components 2.3.0 → 2.4.1

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 (162) hide show
  1. package/dist/components/ps-file-icon-button.js +1 -1
  2. package/dist/components/ps-file-list.js +1 -1
  3. package/dist/components/ps-file-picker.js +1 -1
  4. package/dist/components/ps-file-search-bar.d.ts +10 -0
  5. package/dist/components/ps-file-search-bar.js +25 -0
  6. package/dist/components/ps-file-viewer-content.js +9 -0
  7. package/dist/components/ps-file-viewer-pane.d.ts +1 -1
  8. package/dist/components/ps-file-viewer-pane.js +26 -2
  9. package/dist/components/ps-file-viewer.js +11 -23
  10. package/dist/components/ps-html-document.d.ts +8 -0
  11. package/dist/components/ps-html-document.js +23 -0
  12. package/dist/components/ps-html-viewer.d.ts +2 -0
  13. package/dist/components/ps-html-viewer.js +17 -0
  14. package/dist/components/ps-image-text-highlights.d.ts +9 -0
  15. package/dist/components/ps-image-text-highlights.js +31 -0
  16. package/dist/components/ps-image-viewer.d.ts +1 -1
  17. package/dist/components/ps-image-viewer.js +11 -2
  18. package/dist/components/ps-pdf-page.js +1 -1
  19. package/dist/components/ps-pdf-viewer.js +6 -2
  20. package/dist/components/ps-sheet-viewer.d.ts +2 -0
  21. package/dist/components/ps-sheet-viewer.js +26 -0
  22. package/dist/components/ps-table-view.d.ts +7 -0
  23. package/dist/components/ps-table-view.js +17 -0
  24. package/dist/components/ps-text-viewer.js +7 -14
  25. package/dist/components/ps-upload-row.js +1 -1
  26. package/dist/components/ps-word-viewer.d.ts +2 -0
  27. package/dist/components/ps-word-viewer.js +17 -0
  28. package/dist/constants/ps-file-kind-icons.js +3 -0
  29. package/dist/constants/ps-file-kind.d.ts +3 -0
  30. package/dist/constants/ps-file-kind.js +3 -0
  31. package/dist/constants/ps-file-type-config.d.ts +6 -1
  32. package/dist/constants/ps-file-type-config.js +16 -2
  33. package/dist/constants/ps-file-viewer-config.d.ts +1 -0
  34. package/dist/constants/ps-file-viewer-config.js +1 -0
  35. package/dist/constants/ps-file-viewer-kinds.d.ts +2 -2
  36. package/dist/constants/ps-file-viewer-kinds.js +17 -2
  37. package/dist/constants/ps-file-viewer-labels.d.ts +3 -0
  38. package/dist/constants/ps-file-viewer-labels.js +3 -0
  39. package/dist/constants/ps-html-sanitize-config.d.ts +8 -0
  40. package/dist/constants/ps-html-sanitize-config.js +26 -0
  41. package/dist/esm/components/ps-file-icon-button.js +1 -1
  42. package/dist/esm/components/ps-file-list.js +1 -1
  43. package/dist/esm/components/ps-file-picker.js +1 -1
  44. package/dist/esm/components/ps-file-search-bar.js +21 -0
  45. package/dist/esm/components/ps-file-viewer-content.js +9 -0
  46. package/dist/esm/components/ps-file-viewer-pane.js +27 -3
  47. package/dist/esm/components/ps-file-viewer.js +13 -25
  48. package/dist/esm/components/ps-html-document.js +19 -0
  49. package/dist/esm/components/ps-html-viewer.js +13 -0
  50. package/dist/esm/components/ps-image-text-highlights.js +27 -0
  51. package/dist/esm/components/ps-image-viewer.js +13 -4
  52. package/dist/esm/components/ps-pdf-page.js +1 -1
  53. package/dist/esm/components/ps-pdf-viewer.js +6 -2
  54. package/dist/esm/components/ps-sheet-viewer.js +22 -0
  55. package/dist/esm/components/ps-table-view.js +13 -0
  56. package/dist/esm/components/ps-text-viewer.js +7 -14
  57. package/dist/esm/components/ps-upload-row.js +1 -1
  58. package/dist/esm/components/ps-word-viewer.js +13 -0
  59. package/dist/esm/constants/ps-file-kind-icons.js +3 -0
  60. package/dist/esm/constants/ps-file-kind.js +3 -0
  61. package/dist/esm/constants/ps-file-type-config.js +16 -2
  62. package/dist/esm/constants/ps-file-viewer-config.js +1 -0
  63. package/dist/esm/constants/ps-file-viewer-kinds.js +17 -2
  64. package/dist/esm/constants/ps-file-viewer-labels.js +3 -0
  65. package/dist/esm/constants/ps-html-sanitize-config.js +23 -0
  66. package/dist/esm/hooks/use-ps-file-search.js +37 -0
  67. package/dist/esm/hooks/use-ps-file-text.js +4 -19
  68. package/dist/esm/hooks/use-ps-find-shortcut.js +23 -0
  69. package/dist/esm/hooks/use-ps-loaded-file.js +19 -0
  70. package/dist/esm/index.js +33 -0
  71. package/dist/esm/models/ps-file-search-state.js +1 -0
  72. package/dist/esm/models/ps-html-highlight-result.js +1 -0
  73. package/dist/esm/models/ps-html-text-node.js +1 -0
  74. package/dist/esm/models/ps-image-match.js +1 -0
  75. package/dist/esm/models/ps-image-text-box.js +1 -0
  76. package/dist/esm/models/ps-image-text-layer.js +1 -0
  77. package/dist/esm/models/ps-image-text-word.js +1 -0
  78. package/dist/esm/models/ps-item-match-part.js +1 -0
  79. package/dist/esm/models/ps-item-match.js +1 -0
  80. package/dist/esm/models/ps-loaded-file-state.js +1 -0
  81. package/dist/esm/models/ps-normalized-text.js +1 -0
  82. package/dist/esm/models/ps-pdf-text-item.js +1 -0
  83. package/dist/esm/models/ps-pdf-viewport-like.js +1 -0
  84. package/dist/esm/models/ps-search-item.js +1 -0
  85. package/dist/esm/models/ps-sheet-data.js +1 -0
  86. package/dist/esm/services/ps-file-fetch-service.js +14 -0
  87. package/dist/esm/services/ps-file-search-service.js +15 -0
  88. package/dist/esm/services/ps-file-type-service.js +12 -1
  89. package/dist/esm/services/ps-html-file-service.js +7 -0
  90. package/dist/esm/services/ps-html-service.js +83 -0
  91. package/dist/esm/services/ps-image-text-service.js +6 -0
  92. package/dist/esm/services/ps-label-service.js +5 -0
  93. package/dist/esm/services/ps-pdf-service.js +39 -16
  94. package/dist/esm/services/ps-sheet-service.js +23 -0
  95. package/dist/esm/services/ps-text-search-service.js +21 -26
  96. package/dist/esm/services/ps-word-service.js +15 -0
  97. package/dist/hooks/use-ps-file-search.d.ts +2 -0
  98. package/dist/hooks/use-ps-file-search.js +41 -0
  99. package/dist/hooks/use-ps-file-text.js +4 -19
  100. package/dist/hooks/use-ps-find-shortcut.d.ts +2 -0
  101. package/dist/hooks/use-ps-find-shortcut.js +27 -0
  102. package/dist/hooks/use-ps-loaded-file.d.ts +2 -0
  103. package/dist/hooks/use-ps-loaded-file.js +23 -0
  104. package/dist/index.d.ts +33 -0
  105. package/dist/index.js +33 -0
  106. package/dist/models/ps-file-search-state.d.ts +13 -0
  107. package/dist/models/ps-file-search-state.js +2 -0
  108. package/dist/models/ps-file-viewer-pane-props.d.ts +3 -1
  109. package/dist/models/ps-html-highlight-result.d.ts +4 -0
  110. package/dist/models/ps-html-highlight-result.js +2 -0
  111. package/dist/models/ps-html-text-node.d.ts +4 -0
  112. package/dist/models/ps-html-text-node.js +2 -0
  113. package/dist/models/ps-image-match.d.ts +7 -0
  114. package/dist/models/ps-image-match.js +2 -0
  115. package/dist/models/ps-image-text-box.d.ts +2 -0
  116. package/dist/models/ps-image-text-box.js +2 -0
  117. package/dist/models/ps-image-text-layer.d.ts +2 -0
  118. package/dist/models/ps-image-text-layer.js +2 -0
  119. package/dist/models/ps-image-text-word.d.ts +2 -0
  120. package/dist/models/ps-image-text-word.js +2 -0
  121. package/dist/models/ps-item-match-part.d.ts +2 -0
  122. package/dist/models/ps-item-match-part.js +2 -0
  123. package/dist/models/ps-item-match.d.ts +2 -0
  124. package/dist/models/ps-item-match.js +2 -0
  125. package/dist/models/ps-loaded-file-state.d.ts +4 -0
  126. package/dist/models/ps-loaded-file-state.js +2 -0
  127. package/dist/models/ps-normalized-text.d.ts +2 -0
  128. package/dist/models/ps-normalized-text.js +2 -0
  129. package/dist/models/ps-pdf-match.d.ts +1 -0
  130. package/dist/models/ps-pdf-text-item.d.ts +7 -0
  131. package/dist/models/ps-pdf-text-item.js +2 -0
  132. package/dist/models/ps-pdf-viewport-like.d.ts +5 -0
  133. package/dist/models/ps-pdf-viewport-like.js +2 -0
  134. package/dist/models/ps-search-item.d.ts +2 -0
  135. package/dist/models/ps-search-item.js +2 -0
  136. package/dist/models/ps-sheet-data.d.ts +4 -0
  137. package/dist/models/ps-sheet-data.js +2 -0
  138. package/dist/models/ps-text-match.d.ts +2 -4
  139. package/dist/models/ps-text-segment.d.ts +2 -5
  140. package/dist/models/ps-viewer-file.d.ts +2 -0
  141. package/dist/services/ps-file-fetch-service.d.ts +5 -0
  142. package/dist/services/ps-file-fetch-service.js +18 -0
  143. package/dist/services/ps-file-search-service.d.ts +5 -0
  144. package/dist/services/ps-file-search-service.js +19 -0
  145. package/dist/services/ps-file-type-service.js +12 -1
  146. package/dist/services/ps-html-file-service.d.ts +3 -0
  147. package/dist/services/ps-html-file-service.js +11 -0
  148. package/dist/services/ps-html-service.d.ts +7 -0
  149. package/dist/services/ps-html-service.js +120 -0
  150. package/dist/services/ps-image-text-service.d.ts +5 -0
  151. package/dist/services/ps-image-text-service.js +10 -0
  152. package/dist/services/ps-label-service.d.ts +3 -0
  153. package/dist/services/ps-label-service.js +9 -0
  154. package/dist/services/ps-pdf-service.d.ts +3 -0
  155. package/dist/services/ps-pdf-service.js +39 -16
  156. package/dist/services/ps-sheet-service.d.ts +5 -0
  157. package/dist/services/ps-sheet-service.js +60 -0
  158. package/dist/services/ps-text-search-service.d.ts +7 -0
  159. package/dist/services/ps-text-search-service.js +21 -26
  160. package/dist/services/ps-word-service.d.ts +3 -0
  161. package/dist/services/ps-word-service.js +52 -0
  162. package/package.json +6 -3
@@ -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
+ };
@@ -8,31 +8,24 @@ import { PsCsvService } from '../services/ps-csv-service';
8
8
  import { PsFileTypeService } from '../services/ps-file-type-service';
9
9
  import { PsTextSearchService } from '../services/ps-text-search-service';
10
10
  import { PsHighlightedText } from './ps-highlighted-text';
11
+ import { PsTableView } from './ps-table-view';
11
12
  export const PsTextViewer = ({ activeMatch, file, labels, onMatchCount, query, zoom, }) => {
12
13
  const { hasFailed, text } = usePsFileText(file.url);
13
14
  const container = useRef(null);
14
15
  const isCsv = PsFileTypeService.kindOf(file.contentType, file.name) === PsFileKind.csv;
15
16
  const rows = useMemo(() => (isCsv && text !== null ? PsCsvService.parse(text) : []), [isCsv, text]);
16
- const cells = useMemo(() => {
17
- let offset = 0;
18
- return rows.map((row) => row.map((cell) => {
19
- const matches = PsTextSearchService.find(cell, query);
20
- const segments = PsTextSearchService.segments(cell, matches, offset);
21
- offset += matches.length;
22
- return segments;
23
- }));
24
- }, [rows, query]);
25
17
  const segments = useMemo(() => isCsv || text === null
26
18
  ? []
27
19
  : PsTextSearchService.segments(text, PsTextSearchService.find(text, query)), [isCsv, text, query]);
28
- const count = isCsv
29
- ? cells.reduce((total, row) => total + row.reduce((sum, cell) => sum + cell.filter((part) => part.isMatch).length, 0), 0)
30
- : segments.filter((part) => part.isMatch).length;
31
- useEffect(() => onMatchCount(count), [count]);
20
+ const count = segments.filter((part) => part.isMatch).length;
21
+ useEffect(() => {
22
+ if (!isCsv)
23
+ onMatchCount(count);
24
+ }, [isCsv, count]);
32
25
  usePsActiveMatchScroll(container, activeMatch, query);
33
26
  if (hasFailed)
34
27
  return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
35
28
  if (text === null)
36
29
  return _jsx(PsSpinner, { className: 'm-auto', color: 'current', size: 'large' });
37
- return (_jsx("div", { className: 'h-full w-full overflow-auto', ref: container, children: _jsx("div", { className: 'mx-auto w-full max-w-5xl rounded-lg bg-surface p-5 text-ink shadow-2xl', style: { fontSize: `${zoom * 0.875}rem` }, children: isCsv ? (_jsx("table", { className: 'w-full border-collapse text-left', 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))) }) })) : (_jsx("pre", { className: 'whitespace-pre-wrap break-words font-mono leading-relaxed', children: _jsx(PsHighlightedText, { activeMatch: activeMatch, segments: segments }) })) }) }));
30
+ return (_jsx("div", { className: 'h-full w-full overflow-auto', ref: container, children: _jsx("div", { className: 'mx-auto w-full max-w-5xl rounded-lg bg-surface p-5 text-ink shadow-2xl', style: { fontSize: `${zoom * 0.875}rem` }, children: isCsv ? (_jsx(PsTableView, { activeMatch: activeMatch, onMatchCount: onMatchCount, query: query, rows: rows })) : (_jsx("pre", { className: 'whitespace-pre-wrap break-words font-mono leading-relaxed', children: _jsx(PsHighlightedText, { activeMatch: activeMatch, segments: segments }) })) }) }));
38
31
  };
@@ -16,5 +16,5 @@ export const PsUploadRow = ({ item, labels, onCancel, onRemove, onRetry }) => {
16
16
  : item.status === PsUploadStatus.failed && item.error
17
17
  ? item.error
18
18
  : labels[item.status];
19
- return (_jsxs("li", { className: 'flex items-center gap-3 rounded-ps-control border border-line px-3 py-2.5', children: [_jsx(PsFileKindIcon, { contentType: item.file.type, name: item.file.name }), _jsxs("div", { className: 'flex min-w-0 flex-1 flex-col gap-1.5', children: [_jsx("p", { className: 'truncate text-sm font-medium text-ink', children: item.file.name }), _jsx(PsProgressBar, { label: item.file.name, tone: PsUploadStatusTones[item.status], value: isProblem ? 1 : item.progress }), _jsxs("p", { className: `truncate text-xs ${isProblem ? 'text-danger' : item.status === PsUploadStatus.done ? 'text-success' : 'text-ink/55'}`, role: item.status === PsUploadStatus.failed ? 'alert' : undefined, children: [size, " \u00B7 ", status] })] }), _jsxs("div", { className: 'flex w-20 shrink-0 items-center justify-end gap-1', children: [isProblem ? (_jsx(PsFileIconButton, { iconKey: PsIconKey.Refresh, label: `${labels.retry} ${item.file.name}`, onClick: onRetry })) : null, isActive ? (_jsx(PsFileIconButton, { iconKey: PsIconKey.Close, label: `${labels.cancel} ${item.file.name}`, onClick: onCancel, tone: 'danger' })) : item.status === PsUploadStatus.done ? (_jsx("span", { className: 'ps-control-square ps-control-compact inline-flex items-center justify-center text-success', children: _jsx(PsIcon, { "aria-label": labels.done, className: 'h-4 w-4', iconKey: PsIconKey.Check }) })) : (_jsx(PsFileIconButton, { iconKey: PsIconKey.Close, label: `${labels.remove} ${item.file.name}`, onClick: onRemove }))] })] }));
19
+ return (_jsxs("li", { className: 'flex items-center gap-3 rounded-ps-control border border-line px-3 py-2.5', children: [_jsx(PsFileKindIcon, { contentType: item.file.type, name: item.file.name }), _jsxs("div", { className: 'flex min-w-0 flex-1 flex-col gap-1.5', children: [_jsx("p", { className: 'truncate text-sm font-medium text-ink', children: item.file.name }), _jsx(PsProgressBar, { label: item.file.name, tone: PsUploadStatusTones[item.status], value: isProblem ? 1 : item.progress }), _jsxs("p", { className: `truncate text-xs ${isProblem ? 'text-danger' : item.status === PsUploadStatus.done ? 'text-success' : 'text-muted'}`, role: item.status === PsUploadStatus.failed ? 'alert' : undefined, children: [size, " \u00B7 ", status] })] }), _jsxs("div", { className: 'flex w-20 shrink-0 items-center justify-end gap-1', children: [isProblem ? (_jsx(PsFileIconButton, { iconKey: PsIconKey.Refresh, label: `${labels.retry} ${item.file.name}`, onClick: onRetry })) : null, isActive ? (_jsx(PsFileIconButton, { iconKey: PsIconKey.Close, label: `${labels.cancel} ${item.file.name}`, onClick: onCancel, tone: 'danger' })) : item.status === PsUploadStatus.done ? (_jsx("span", { className: 'ps-control-square ps-control-compact inline-flex items-center justify-center text-success', children: _jsx(PsIcon, { "aria-label": labels.done, className: 'h-4 w-4', iconKey: PsIconKey.Check }) })) : (_jsx(PsFileIconButton, { iconKey: PsIconKey.Close, label: `${labels.remove} ${item.file.name}`, onClick: onRemove }))] })] }));
20
20
  };
@@ -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 { PsWordService } from '../services/ps-word-service';
5
+ import { PsHtmlDocument } from './ps-html-document';
6
+ export const PsWordViewer = ({ activeMatch, file, labels, onMatchCount, query, zoom, }) => {
7
+ const { hasFailed, value } = usePsLoadedFile(file.url, PsWordService.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
+ };
@@ -3,9 +3,12 @@ import { PsFileKind } from './ps-file-kind';
3
3
  export const PsFileKindIcons = {
4
4
  [PsFileKind.audio]: PsIconKey.Audio,
5
5
  [PsFileKind.csv]: PsIconKey.Sheet,
6
+ [PsFileKind.html]: PsIconKey.Text,
6
7
  [PsFileKind.image]: PsIconKey.Image,
7
8
  [PsFileKind.other]: PsIconKey.File,
8
9
  [PsFileKind.pdf]: PsIconKey.Doc,
10
+ [PsFileKind.sheet]: PsIconKey.Sheet,
9
11
  [PsFileKind.text]: PsIconKey.Text,
10
12
  [PsFileKind.video]: PsIconKey.Video,
13
+ [PsFileKind.word]: PsIconKey.Doc,
11
14
  };
@@ -1,9 +1,12 @@
1
1
  export const PsFileKind = {
2
2
  audio: 'audio',
3
3
  csv: 'csv',
4
+ html: 'html',
4
5
  image: 'image',
5
6
  other: 'other',
6
7
  pdf: 'pdf',
8
+ sheet: 'sheet',
7
9
  text: 'text',
8
10
  video: 'video',
11
+ word: 'word',
9
12
  };
@@ -1,26 +1,40 @@
1
1
  import { PsFileKind } from './ps-file-kind';
2
2
  export const PsFileTypeConfig = {
3
- csvTypes: ['text/csv', 'application/csv', 'application/vnd.ms-excel'],
3
+ csvTypes: ['text/csv', 'application/csv'],
4
+ excelLegacyType: 'application/vnd.ms-excel',
4
5
  extensionKinds: {
6
+ aac: PsFileKind.audio,
7
+ avif: PsFileKind.image,
8
+ bmp: PsFileKind.image,
5
9
  csv: PsFileKind.csv,
10
+ docx: PsFileKind.word,
6
11
  gif: PsFileKind.image,
7
- heic: PsFileKind.image,
12
+ htm: PsFileKind.html,
13
+ html: PsFileKind.html,
8
14
  jpeg: PsFileKind.image,
9
15
  jpg: PsFileKind.image,
10
16
  json: PsFileKind.text,
11
17
  log: PsFileKind.text,
18
+ m4a: PsFileKind.audio,
12
19
  md: PsFileKind.text,
13
20
  mov: PsFileKind.video,
14
21
  mp3: PsFileKind.audio,
15
22
  mp4: PsFileKind.video,
23
+ ogg: PsFileKind.audio,
16
24
  pdf: PsFileKind.pdf,
17
25
  png: PsFileKind.image,
18
26
  svg: PsFileKind.image,
27
+ tsv: PsFileKind.text,
19
28
  txt: PsFileKind.text,
20
29
  wav: PsFileKind.audio,
21
30
  webm: PsFileKind.video,
22
31
  webp: PsFileKind.image,
32
+ xlsx: PsFileKind.sheet,
23
33
  xml: PsFileKind.text,
24
34
  },
35
+ htmlTypes: ['text/html', 'application/xhtml+xml'],
36
+ sheetTypes: ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
25
37
  textTypes: ['application/json', 'application/xml'],
38
+ unsupportedImageTypes: ['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence'],
39
+ wordTypes: ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
26
40
  };
@@ -1,5 +1,6 @@
1
1
  export const PsFileViewerConfig = {
2
2
  dragActivationDistance: 4,
3
+ maxSheetRows: 2000,
3
4
  maxZoom: 4,
4
5
  minZoom: 0.25,
5
6
  pdfBaseScale: 1.4,
@@ -1,6 +1,21 @@
1
1
  import { PsFileKind } from './ps-file-kind';
2
2
  export const PsFileViewerKinds = {
3
3
  rotatable: [PsFileKind.image, PsFileKind.pdf],
4
- searchable: [PsFileKind.csv, PsFileKind.pdf, PsFileKind.text],
5
- zoomable: [PsFileKind.csv, PsFileKind.image, PsFileKind.pdf, PsFileKind.text],
4
+ searchable: [
5
+ PsFileKind.csv,
6
+ PsFileKind.html,
7
+ PsFileKind.pdf,
8
+ PsFileKind.sheet,
9
+ PsFileKind.text,
10
+ PsFileKind.word,
11
+ ],
12
+ zoomable: [
13
+ PsFileKind.csv,
14
+ PsFileKind.html,
15
+ PsFileKind.image,
16
+ PsFileKind.pdf,
17
+ PsFileKind.sheet,
18
+ PsFileKind.text,
19
+ PsFileKind.word,
20
+ ],
6
21
  };
@@ -1,11 +1,13 @@
1
1
  export const PsFileViewerLabels = {
2
2
  close: 'Close',
3
+ closeSearch: 'Close search',
3
4
  delete: 'Delete',
4
5
  deleteConfirm: 'Delete file',
5
6
  deleteConfirmBody: 'will be deleted. You cannot undo this.',
6
7
  deleteCancel: 'Cancel',
7
8
  download: 'Download',
8
9
  loadFailed: 'This file could not be opened.',
10
+ matchCount: '{current} of {total}',
9
11
  next: 'Next file',
10
12
  nextMatch: 'Next match',
11
13
  noMatches: 'No matches',
@@ -14,6 +16,7 @@ export const PsFileViewerLabels = {
14
16
  previousMatch: 'Previous match',
15
17
  rotate: 'Rotate',
16
18
  search: 'Search in file',
19
+ sheetTruncated: 'Showing the first {count} rows',
17
20
  unsupported: 'This file type cannot be previewed here. Download it to open it.',
18
21
  zoomIn: 'Zoom in',
19
22
  zoomOut: 'Zoom out',
@@ -0,0 +1,23 @@
1
+ export const PsHtmlSanitizeConfig = {
2
+ anchorRel: 'noopener noreferrer',
3
+ anchorTarget: '_blank',
4
+ blockSelector: 'p,li,td,th,tr,h1,h2,h3,h4,h5,h6,div,pre,blockquote,section,article,caption',
5
+ forbiddenAttributes: ['style', 'srcset'],
6
+ forbiddenTags: [
7
+ 'style',
8
+ 'form',
9
+ 'input',
10
+ 'button',
11
+ 'textarea',
12
+ 'select',
13
+ 'iframe',
14
+ 'frame',
15
+ 'object',
16
+ 'embed',
17
+ 'link',
18
+ 'meta',
19
+ 'base',
20
+ 'script',
21
+ ],
22
+ mediaTags: ['img', 'picture', 'source', 'video', 'audio', 'track'],
23
+ };
@@ -0,0 +1,37 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { usePsDebouncedValue } from '@pallasoft/ps-core-components';
3
+ import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
4
+ export const usePsFileSearch = (resetKey) => {
5
+ const [search, setSearch] = useState('');
6
+ const [activeMatch, setActiveMatch] = useState(0);
7
+ const [matchCount, setMatchCount] = useState(0);
8
+ const inputRef = useRef(null);
9
+ const query = usePsDebouncedValue(search, PsFileViewerConfig.searchDebounceMs);
10
+ const reset = useCallback(() => {
11
+ setSearch('');
12
+ setActiveMatch(0);
13
+ setMatchCount(0);
14
+ }, []);
15
+ useEffect(reset, [resetKey]);
16
+ useEffect(() => setActiveMatch(0), [query]);
17
+ const focus = useCallback(() => {
18
+ inputRef.current?.focus();
19
+ inputRef.current?.select();
20
+ }, []);
21
+ const step = useCallback((delta) => {
22
+ if (matchCount > 0)
23
+ setActiveMatch((current) => (current + delta + matchCount) % matchCount);
24
+ }, [matchCount]);
25
+ return {
26
+ activeMatch,
27
+ focus,
28
+ inputRef,
29
+ matchCount,
30
+ query,
31
+ reset,
32
+ search,
33
+ setMatchCount,
34
+ setSearch,
35
+ step,
36
+ };
37
+ };
@@ -1,21 +1,6 @@
1
- import { useEffect, useState } from 'react';
1
+ import { PsFileFetchService } from '../services/ps-file-fetch-service';
2
+ import { usePsLoadedFile } from './use-ps-loaded-file';
2
3
  export const usePsFileText = (url) => {
3
- const [state, setState] = useState({ hasFailed: false, text: null });
4
- useEffect(() => {
5
- const controller = new AbortController();
6
- setState({ hasFailed: false, text: null });
7
- fetch(url, { credentials: 'include', signal: controller.signal })
8
- .then((response) => {
9
- if (!response.ok)
10
- throw new Error(response.statusText);
11
- return response.text();
12
- })
13
- .then((text) => setState({ hasFailed: false, text }))
14
- .catch(() => {
15
- if (!controller.signal.aborted)
16
- setState({ hasFailed: true, text: null });
17
- });
18
- return () => controller.abort();
19
- }, [url]);
20
- return state;
4
+ const { hasFailed, value } = usePsLoadedFile(url, PsFileFetchService.text);
5
+ return { hasFailed, text: value };
21
6
  };
@@ -0,0 +1,23 @@
1
+ import { useEffect } from 'react';
2
+ import { PsFileSearchService } from '../services/ps-file-search-service';
3
+ export const usePsFindShortcut = (scope, isEnabled, onFind) => {
4
+ useEffect(() => {
5
+ if (!isEnabled)
6
+ return;
7
+ const onKey = (event) => {
8
+ if (!PsFileSearchService.isFindShortcut(event))
9
+ return;
10
+ if (scope !== null) {
11
+ const node = scope.current;
12
+ if (node === null)
13
+ return;
14
+ if (!node.contains(document.activeElement) && !node.matches(':hover'))
15
+ return;
16
+ }
17
+ event.preventDefault();
18
+ onFind();
19
+ };
20
+ window.addEventListener('keydown', onKey);
21
+ return () => window.removeEventListener('keydown', onKey);
22
+ }, [scope, isEnabled, onFind]);
23
+ };
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from 'react';
2
+ export const usePsLoadedFile = (url, load) => {
3
+ const [state, setState] = useState({ hasFailed: false, value: null });
4
+ useEffect(() => {
5
+ const controller = new AbortController();
6
+ setState({ hasFailed: false, value: null });
7
+ load(url, controller.signal)
8
+ .then((value) => {
9
+ if (!controller.signal.aborted)
10
+ setState({ hasFailed: false, value });
11
+ })
12
+ .catch(() => {
13
+ if (!controller.signal.aborted)
14
+ setState({ hasFailed: true, value: null });
15
+ });
16
+ return () => controller.abort();
17
+ }, [url, load]);
18
+ return state;
19
+ };
package/dist/esm/index.js CHANGED
@@ -18,6 +18,13 @@ export * from './components/ps-text-viewer';
18
18
  export * from './components/ps-unsupported-viewer';
19
19
  export * from './components/ps-upload-modal';
20
20
  export * from './components/ps-upload-row';
21
+ export * from './components/ps-file-search-bar';
22
+ export * from './components/ps-html-document';
23
+ export * from './components/ps-html-viewer';
24
+ export * from './components/ps-image-text-highlights';
25
+ export * from './components/ps-sheet-viewer';
26
+ export * from './components/ps-table-view';
27
+ export * from './components/ps-word-viewer';
21
28
  export * from './constants/ps-file-icon-button-classes';
22
29
  export * from './constants/ps-file-kind-icons';
23
30
  export * from './constants/ps-file-kind';
@@ -33,6 +40,7 @@ export * from './constants/ps-upload-messages';
33
40
  export * from './constants/ps-upload-queue-config';
34
41
  export * from './constants/ps-upload-status-tones';
35
42
  export * from './constants/ps-upload-status';
43
+ export * from './constants/ps-html-sanitize-config';
36
44
  export * from './models/ps-drag-pan-handlers';
37
45
  export * from './models/ps-drag-pan-options';
38
46
  export * from './models/ps-file-icon-button-tone';
@@ -59,6 +67,20 @@ export * from './models/ps-viewer-file';
59
67
  export * from './models/ps-xhr-response-type';
60
68
  export * from './models/ps-xhr-upload-request';
61
69
  export * from './models/ps-xhr-upload-response';
70
+ export * from './models/ps-file-search-state';
71
+ export * from './models/ps-html-highlight-result';
72
+ export * from './models/ps-image-match';
73
+ export * from './models/ps-image-text-box';
74
+ export * from './models/ps-image-text-layer';
75
+ export * from './models/ps-image-text-word';
76
+ export * from './models/ps-item-match';
77
+ export * from './models/ps-item-match-part';
78
+ export * from './models/ps-loaded-file-state';
79
+ export * from './models/ps-normalized-text';
80
+ export * from './models/ps-pdf-text-item';
81
+ export * from './models/ps-pdf-viewport-like';
82
+ export * from './models/ps-search-item';
83
+ export * from './models/ps-sheet-data';
62
84
  export * from './services/ps-csv-service';
63
85
  export * from './services/ps-file-selection-service';
64
86
  export * from './services/ps-file-size-service';
@@ -67,11 +89,22 @@ export * from './services/ps-pdf-service';
67
89
  export * from './services/ps-text-search-service';
68
90
  export * from './services/ps-upload-queue-service';
69
91
  export * from './services/ps-xhr-upload-service';
92
+ export * from './services/ps-file-fetch-service';
93
+ export * from './services/ps-file-search-service';
94
+ export * from './services/ps-html-file-service';
95
+ export * from './services/ps-html-service';
96
+ export * from './services/ps-image-text-service';
97
+ export * from './services/ps-label-service';
98
+ export * from './services/ps-sheet-service';
99
+ export * from './services/ps-word-service';
70
100
  export * from './hooks/use-ps-active-match-scroll';
71
101
  export * from './hooks/use-ps-drag-pan';
72
102
  export * from './hooks/use-ps-file-text';
73
103
  export * from './hooks/use-ps-in-view';
74
104
  export * from './hooks/use-ps-upload-queue';
105
+ export * from './hooks/use-ps-file-search';
106
+ export * from './hooks/use-ps-find-shortcut';
107
+ export * from './hooks/use-ps-loaded-file';
75
108
  export * from './components/ps-document-scanner';
76
109
  export * from './constants/ps-document-scanner-labels';
77
110
  export * from './constants/ps-scan-format';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export class PsFileFetchService {
2
+ static async response(url, signal) {
3
+ const response = await fetch(url, { credentials: 'include', signal });
4
+ if (!response.ok)
5
+ throw new Error(response.statusText);
6
+ return response;
7
+ }
8
+ static async text(url, signal) {
9
+ return (await PsFileFetchService.response(url, signal)).text();
10
+ }
11
+ static async buffer(url, signal) {
12
+ return (await PsFileFetchService.response(url, signal)).arrayBuffer();
13
+ }
14
+ }
@@ -0,0 +1,15 @@
1
+ import { PsFileKind } from '../constants/ps-file-kind';
2
+ import { PsFileViewerKinds } from '../constants/ps-file-viewer-kinds';
3
+ export class PsFileSearchService {
4
+ static canSearch(kind, hasTextLayer) {
5
+ if (PsFileViewerKinds.searchable.includes(kind))
6
+ return true;
7
+ return kind === PsFileKind.image && hasTextLayer;
8
+ }
9
+ static isFindShortcut(event) {
10
+ return ((event.metaKey || event.ctrlKey) &&
11
+ !event.altKey &&
12
+ !event.shiftKey &&
13
+ event.key.toLowerCase() === 'f');
14
+ }
15
+ }
@@ -7,10 +7,21 @@ export class PsFileTypeService {
7
7
  }
8
8
  static kindOf(contentType, name) {
9
9
  const type = contentType.toLowerCase();
10
+ const extension = PsFileTypeService.extension(name);
10
11
  if (type === 'application/pdf')
11
12
  return PsFileKind.pdf;
12
13
  if (PsFileTypeConfig.csvTypes.includes(type))
13
14
  return PsFileKind.csv;
15
+ if (type === PsFileTypeConfig.excelLegacyType)
16
+ return extension === 'xls' || extension === 'xlsb' ? PsFileKind.other : PsFileKind.csv;
17
+ if (PsFileTypeConfig.sheetTypes.includes(type))
18
+ return PsFileKind.sheet;
19
+ if (PsFileTypeConfig.wordTypes.includes(type))
20
+ return PsFileKind.word;
21
+ if (PsFileTypeConfig.htmlTypes.includes(type))
22
+ return PsFileKind.html;
23
+ if (PsFileTypeConfig.unsupportedImageTypes.includes(type))
24
+ return PsFileKind.other;
14
25
  if (type.startsWith('image/'))
15
26
  return PsFileKind.image;
16
27
  if (type.startsWith('video/'))
@@ -20,7 +31,7 @@ export class PsFileTypeService {
20
31
  if (type.startsWith('text/') ||
21
32
  PsFileTypeConfig.textTypes.includes(type))
22
33
  return PsFileKind.text;
23
- const byExtension = PsFileTypeConfig.extensionKinds[PsFileTypeService.extension(name)];
34
+ const byExtension = PsFileTypeConfig.extensionKinds[extension];
24
35
  return byExtension === undefined ? PsFileKind.other : byExtension;
25
36
  }
26
37
  static isAccepted(file, accept) {
@@ -0,0 +1,7 @@
1
+ import { PsFileFetchService } from './ps-file-fetch-service';
2
+ import { PsHtmlService } from './ps-html-service';
3
+ export class PsHtmlFileService {
4
+ static async load(url, signal) {
5
+ return PsHtmlService.sanitize(await PsFileFetchService.text(url, signal), false);
6
+ }
7
+ }