@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
@@ -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
  };
@@ -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
+ }
@@ -0,0 +1,83 @@
1
+ import { PsHtmlSanitizeConfig } from '../constants/ps-html-sanitize-config';
2
+ import { PsTextSearchService } from './ps-text-search-service';
3
+ export class PsHtmlService {
4
+ static async sanitize(html, allowImages) {
5
+ const library = await import('dompurify');
6
+ const forbidden = [...PsHtmlSanitizeConfig.forbiddenTags];
7
+ if (!allowImages)
8
+ forbidden.push(...PsHtmlSanitizeConfig.mediaTags);
9
+ const clean = library.default.sanitize(html, {
10
+ FORBID_ATTR: [...PsHtmlSanitizeConfig.forbiddenAttributes],
11
+ FORBID_TAGS: forbidden,
12
+ USE_PROFILES: { html: true },
13
+ });
14
+ return PsHtmlService.harden(clean);
15
+ }
16
+ static harden(html) {
17
+ const template = document.createElement('template');
18
+ template.innerHTML = html;
19
+ template.content.querySelectorAll('a').forEach((anchor) => {
20
+ anchor.setAttribute('target', PsHtmlSanitizeConfig.anchorTarget);
21
+ anchor.setAttribute('rel', PsHtmlSanitizeConfig.anchorRel);
22
+ });
23
+ template.content.querySelectorAll('img').forEach((image) => {
24
+ const source = image.getAttribute('src');
25
+ if (source === null || !source.startsWith('data:'))
26
+ image.remove();
27
+ });
28
+ return template.innerHTML;
29
+ }
30
+ static highlight(html, query) {
31
+ if (query.trim().length === 0)
32
+ return { count: 0, html };
33
+ const template = document.createElement('template');
34
+ template.innerHTML = html;
35
+ const nodes = [];
36
+ const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT);
37
+ let full = '';
38
+ let previousBlock = null;
39
+ let current = walker.nextNode();
40
+ while (current !== null) {
41
+ const node = current;
42
+ const block = node.parentElement === null
43
+ ? null
44
+ : node.parentElement.closest(PsHtmlSanitizeConfig.blockSelector);
45
+ if (nodes.length > 0 && block !== previousBlock)
46
+ full += '\n';
47
+ nodes.push({ node, offset: full.length });
48
+ full += node.data;
49
+ previousBlock = block;
50
+ current = walker.nextNode();
51
+ }
52
+ const matches = PsTextSearchService.find(full, query);
53
+ if (matches.length === 0)
54
+ return { count: 0, html };
55
+ nodes.forEach(({ node, offset }) => PsHtmlService.wrap(node, offset, matches));
56
+ return { count: matches.length, html: template.innerHTML };
57
+ }
58
+ static wrap(node, offset, matches) {
59
+ const end = offset + node.data.length;
60
+ const fragment = document.createDocumentFragment();
61
+ let cursor = 0;
62
+ let hasMark = false;
63
+ matches.forEach((match, matchIndex) => {
64
+ const start = Math.max(match.start, offset) - offset;
65
+ const stop = Math.min(match.end, end) - offset;
66
+ if (stop <= start || match.start >= end || match.end <= offset)
67
+ return;
68
+ if (start > cursor)
69
+ fragment.append(node.data.slice(cursor, start));
70
+ const mark = document.createElement('mark');
71
+ mark.setAttribute('data-ps-match', String(matchIndex));
72
+ mark.textContent = node.data.slice(start, stop);
73
+ fragment.append(mark);
74
+ cursor = stop;
75
+ hasMark = true;
76
+ });
77
+ if (!hasMark)
78
+ return;
79
+ if (cursor < node.data.length)
80
+ fragment.append(node.data.slice(cursor));
81
+ node.replaceWith(fragment);
82
+ }
83
+ }
@@ -0,0 +1,28 @@
1
+ import { PsTextSearchService } from './ps-text-search-service';
2
+ export class PsImageTextService {
3
+ static matches(layer, query) {
4
+ if (layer.width <= 0 || layer.height <= 0)
5
+ return [];
6
+ const items = layer.words.map((word, index) => ({
7
+ isBreakAfter: index < layer.words.length - 1,
8
+ text: word.text,
9
+ }));
10
+ const rects = [];
11
+ PsTextSearchService.findInItems(items, query).forEach((match, matchIndex) => {
12
+ match.parts.forEach((part) => {
13
+ const word = layer.words[part.itemIndex];
14
+ const length = word.text.length;
15
+ const left = word.box.left + (word.box.width * part.start) / length;
16
+ const width = (word.box.width * (part.end - part.start)) / length;
17
+ rects.push({
18
+ height: word.box.height / layer.height,
19
+ left: left / layer.width,
20
+ matchIndex,
21
+ top: word.box.top / layer.height,
22
+ width: width / layer.width,
23
+ });
24
+ });
25
+ });
26
+ return rects;
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ export class PsLabelService {
2
+ static fill(template, values) {
3
+ return Object.keys(values).reduce((text, key) => text.split(`{${key}}`).join(String(values[key])), template);
4
+ }
5
+ }