@pallasoft/ps-file-components 1.0.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.
- package/LICENSE +32 -0
- package/README.md +10 -0
- package/dist/components/ps-file-delete-confirm.d.ts +11 -0
- package/dist/components/ps-file-delete-confirm.js +8 -0
- package/dist/components/ps-file-kind-icon.d.ts +6 -0
- package/dist/components/ps-file-kind-icon.js +10 -0
- package/dist/components/ps-file-list.d.ts +12 -0
- package/dist/components/ps-file-list.js +27 -0
- package/dist/components/ps-file-picker.d.ts +18 -0
- package/dist/components/ps-file-picker.js +48 -0
- package/dist/components/ps-file-viewer-button.d.ts +10 -0
- package/dist/components/ps-file-viewer-button.js +7 -0
- package/dist/components/ps-file-viewer-content.d.ts +2 -0
- package/dist/components/ps-file-viewer-content.js +28 -0
- package/dist/components/ps-file-viewer.d.ts +14 -0
- package/dist/components/ps-file-viewer.js +102 -0
- package/dist/components/ps-progress-bar.d.ts +7 -0
- package/dist/components/ps-progress-bar.js +15 -0
- package/dist/components/ps-upload-modal.d.ts +10 -0
- package/dist/components/ps-upload-modal.js +19 -0
- package/dist/components/ps-upload-row.d.ts +11 -0
- package/dist/components/ps-upload-row.js +28 -0
- package/dist/components/viewers/ps-highlighted-text.d.ts +6 -0
- package/dist/components/viewers/ps-highlighted-text.js +8 -0
- package/dist/components/viewers/ps-image-viewer.d.ts +2 -0
- package/dist/components/viewers/ps-image-viewer.js +33 -0
- package/dist/components/viewers/ps-media-viewer.d.ts +2 -0
- package/dist/components/viewers/ps-media-viewer.js +8 -0
- package/dist/components/viewers/ps-pdf-page.d.ts +12 -0
- package/dist/components/viewers/ps-pdf-page.js +37 -0
- package/dist/components/viewers/ps-pdf-thumbnail.d.ts +11 -0
- package/dist/components/viewers/ps-pdf-thumbnail.js +28 -0
- package/dist/components/viewers/ps-pdf-viewer.d.ts +2 -0
- package/dist/components/viewers/ps-pdf-viewer.js +100 -0
- package/dist/components/viewers/ps-text-viewer.d.ts +2 -0
- package/dist/components/viewers/ps-text-viewer.js +42 -0
- package/dist/components/viewers/ps-unsupported-viewer.d.ts +2 -0
- package/dist/components/viewers/ps-unsupported-viewer.js +8 -0
- package/dist/constants/ps-file-kind-icons.d.ts +3 -0
- package/dist/constants/ps-file-kind-icons.js +14 -0
- package/dist/constants/ps-file-kind.d.ts +10 -0
- package/dist/constants/ps-file-kind.js +12 -0
- package/dist/constants/ps-file-list-labels.d.ts +6 -0
- package/dist/constants/ps-file-list-labels.js +9 -0
- package/dist/constants/ps-file-picker-labels.d.ts +7 -0
- package/dist/constants/ps-file-picker-labels.js +10 -0
- package/dist/constants/ps-file-type-config.d.ts +7 -0
- package/dist/constants/ps-file-type-config.js +30 -0
- package/dist/constants/ps-file-viewer-config.d.ts +9 -0
- package/dist/constants/ps-file-viewer-config.js +12 -0
- package/dist/constants/ps-file-viewer-kinds.d.ts +5 -0
- package/dist/constants/ps-file-viewer-kinds.js +9 -0
- package/dist/constants/ps-file-viewer-labels.d.ts +21 -0
- package/dist/constants/ps-file-viewer-labels.js +24 -0
- package/dist/constants/ps-upload-labels.d.ts +14 -0
- package/dist/constants/ps-upload-labels.js +17 -0
- package/dist/constants/ps-upload-messages.d.ts +4 -0
- package/dist/constants/ps-upload-messages.js +7 -0
- package/dist/constants/ps-upload-queue-config.d.ts +4 -0
- package/dist/constants/ps-upload-queue-config.js +7 -0
- package/dist/constants/ps-upload-status-tones.d.ts +2 -0
- package/dist/constants/ps-upload-status-tones.js +12 -0
- package/dist/constants/ps-upload-status.d.ts +9 -0
- package/dist/constants/ps-upload-status.js +11 -0
- package/dist/esm/components/ps-file-delete-confirm.js +4 -0
- package/dist/esm/components/ps-file-kind-icon.js +6 -0
- package/dist/esm/components/ps-file-list.js +23 -0
- package/dist/esm/components/ps-file-picker.js +44 -0
- package/dist/esm/components/ps-file-viewer-button.js +3 -0
- package/dist/esm/components/ps-file-viewer-content.js +24 -0
- package/dist/esm/components/ps-file-viewer.js +98 -0
- package/dist/esm/components/ps-progress-bar.js +11 -0
- package/dist/esm/components/ps-upload-modal.js +15 -0
- package/dist/esm/components/ps-upload-row.js +24 -0
- package/dist/esm/components/viewers/ps-highlighted-text.js +4 -0
- package/dist/esm/components/viewers/ps-image-viewer.js +29 -0
- package/dist/esm/components/viewers/ps-media-viewer.js +4 -0
- package/dist/esm/components/viewers/ps-pdf-page.js +33 -0
- package/dist/esm/components/viewers/ps-pdf-thumbnail.js +24 -0
- package/dist/esm/components/viewers/ps-pdf-viewer.js +96 -0
- package/dist/esm/components/viewers/ps-text-viewer.js +38 -0
- package/dist/esm/components/viewers/ps-unsupported-viewer.js +4 -0
- package/dist/esm/constants/ps-file-kind-icons.js +11 -0
- package/dist/esm/constants/ps-file-kind.js +9 -0
- package/dist/esm/constants/ps-file-list-labels.js +6 -0
- package/dist/esm/constants/ps-file-picker-labels.js +7 -0
- package/dist/esm/constants/ps-file-type-config.js +27 -0
- package/dist/esm/constants/ps-file-viewer-config.js +9 -0
- package/dist/esm/constants/ps-file-viewer-kinds.js +6 -0
- package/dist/esm/constants/ps-file-viewer-labels.js +21 -0
- package/dist/esm/constants/ps-upload-labels.js +14 -0
- package/dist/esm/constants/ps-upload-messages.js +4 -0
- package/dist/esm/constants/ps-upload-queue-config.js +4 -0
- package/dist/esm/constants/ps-upload-status-tones.js +9 -0
- package/dist/esm/constants/ps-upload-status.js +8 -0
- package/dist/esm/hooks/use-ps-active-match-scroll.js +8 -0
- package/dist/esm/hooks/use-ps-file-text.js +21 -0
- package/dist/esm/hooks/use-ps-in-view.js +17 -0
- package/dist/esm/hooks/use-ps-upload-queue.js +67 -0
- package/dist/esm/index.js +62 -0
- package/dist/esm/models/ps-file-rejection.js +1 -0
- package/dist/esm/models/ps-file-selection-rules.js +1 -0
- package/dist/esm/models/ps-file-selection.js +1 -0
- package/dist/esm/models/ps-file-text-state.js +1 -0
- package/dist/esm/models/ps-file-viewer-content-props.js +1 -0
- package/dist/esm/models/ps-label-set.js +1 -0
- package/dist/esm/models/ps-page-size.js +1 -0
- package/dist/esm/models/ps-pdf-match.js +1 -0
- package/dist/esm/models/ps-point.js +1 -0
- package/dist/esm/models/ps-text-match.js +1 -0
- package/dist/esm/models/ps-text-segment.js +1 -0
- package/dist/esm/models/ps-upload-handler-options.js +1 -0
- package/dist/esm/models/ps-upload-item.js +1 -0
- package/dist/esm/models/ps-upload-queue-options.js +1 -0
- package/dist/esm/models/ps-upload-queue-state.js +1 -0
- package/dist/esm/models/ps-upload-summary.js +1 -0
- package/dist/esm/models/ps-viewer-file.js +1 -0
- package/dist/esm/models/ps-xhr-upload-request.js +1 -0
- package/dist/esm/models/ps-xhr-upload-response.js +1 -0
- package/dist/esm/services/ps-csv-service.js +42 -0
- package/dist/esm/services/ps-file-selection-service.js +16 -0
- package/dist/esm/services/ps-file-size-service.js +14 -0
- package/dist/esm/services/ps-file-type-service.js +38 -0
- package/dist/esm/services/ps-pdf-service.js +59 -0
- package/dist/esm/services/ps-text-search-service.js +32 -0
- package/dist/esm/services/ps-upload-queue-service.js +37 -0
- package/dist/esm/services/ps-xhr-upload-service.js +52 -0
- package/dist/hooks/use-ps-active-match-scroll.d.ts +2 -0
- package/dist/hooks/use-ps-active-match-scroll.js +12 -0
- package/dist/hooks/use-ps-file-text.d.ts +2 -0
- package/dist/hooks/use-ps-file-text.js +25 -0
- package/dist/hooks/use-ps-in-view.d.ts +2 -0
- package/dist/hooks/use-ps-in-view.js +21 -0
- package/dist/hooks/use-ps-upload-queue.d.ts +3 -0
- package/dist/hooks/use-ps-upload-queue.js +71 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +78 -0
- package/dist/models/ps-file-rejection.d.ts +4 -0
- package/dist/models/ps-file-rejection.js +2 -0
- package/dist/models/ps-file-selection-rules.d.ts +6 -0
- package/dist/models/ps-file-selection-rules.js +2 -0
- package/dist/models/ps-file-selection.d.ts +5 -0
- package/dist/models/ps-file-selection.js +2 -0
- package/dist/models/ps-file-text-state.d.ts +4 -0
- package/dist/models/ps-file-text-state.js +2 -0
- package/dist/models/ps-file-viewer-content-props.d.ts +13 -0
- package/dist/models/ps-file-viewer-content-props.js +2 -0
- package/dist/models/ps-label-set.d.ts +3 -0
- package/dist/models/ps-label-set.js +2 -0
- package/dist/models/ps-page-size.d.ts +4 -0
- package/dist/models/ps-page-size.js +2 -0
- package/dist/models/ps-pdf-match.d.ts +7 -0
- package/dist/models/ps-pdf-match.js +2 -0
- package/dist/models/ps-point.d.ts +4 -0
- package/dist/models/ps-point.js +2 -0
- package/dist/models/ps-text-match.d.ts +4 -0
- package/dist/models/ps-text-match.js +2 -0
- package/dist/models/ps-text-segment.d.ts +5 -0
- package/dist/models/ps-text-segment.js +2 -0
- package/dist/models/ps-upload-handler-options.d.ts +4 -0
- package/dist/models/ps-upload-handler-options.js +2 -0
- package/dist/models/ps-upload-item.d.ts +8 -0
- package/dist/models/ps-upload-item.js +2 -0
- package/dist/models/ps-upload-queue-options.d.ts +6 -0
- package/dist/models/ps-upload-queue-options.js +2 -0
- package/dist/models/ps-upload-queue-state.d.ts +10 -0
- package/dist/models/ps-upload-queue-state.js +2 -0
- package/dist/models/ps-upload-summary.d.ts +5 -0
- package/dist/models/ps-upload-summary.js +2 -0
- package/dist/models/ps-viewer-file.d.ts +9 -0
- package/dist/models/ps-viewer-file.js +2 -0
- package/dist/models/ps-xhr-upload-request.d.ts +8 -0
- package/dist/models/ps-xhr-upload-request.js +2 -0
- package/dist/models/ps-xhr-upload-response.d.ts +4 -0
- package/dist/models/ps-xhr-upload-response.js +2 -0
- package/dist/services/ps-csv-service.d.ts +3 -0
- package/dist/services/ps-csv-service.js +46 -0
- package/dist/services/ps-file-selection-service.d.ts +5 -0
- package/dist/services/ps-file-selection-service.js +20 -0
- package/dist/services/ps-file-size-service.d.ts +3 -0
- package/dist/services/ps-file-size-service.js +18 -0
- package/dist/services/ps-file-type-service.d.ts +6 -0
- package/dist/services/ps-file-type-service.js +42 -0
- package/dist/services/ps-pdf-service.d.ts +11 -0
- package/dist/services/ps-pdf-service.js +96 -0
- package/dist/services/ps-text-search-service.d.ts +6 -0
- package/dist/services/ps-text-search-service.js +36 -0
- package/dist/services/ps-upload-queue-service.d.ts +10 -0
- package/dist/services/ps-upload-queue-service.js +41 -0
- package/dist/services/ps-xhr-upload-service.d.ts +7 -0
- package/dist/services/ps-xhr-upload-service.js +56 -0
- package/package.json +85 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PsFileKind } from '../constants/ps-file-kind';
|
|
3
|
+
import { PsFileTypeService } from '../services/ps-file-type-service';
|
|
4
|
+
import { PsImageViewer } from './viewers/ps-image-viewer';
|
|
5
|
+
import { PsMediaViewer } from './viewers/ps-media-viewer';
|
|
6
|
+
import { PsPdfViewer } from './viewers/ps-pdf-viewer';
|
|
7
|
+
import { PsTextViewer } from './viewers/ps-text-viewer';
|
|
8
|
+
import { PsUnsupportedViewer } from './viewers/ps-unsupported-viewer';
|
|
9
|
+
export const PsFileViewerContent = (props) => {
|
|
10
|
+
switch (PsFileTypeService.kindOf(props.file.contentType, props.file.name)) {
|
|
11
|
+
case PsFileKind.image:
|
|
12
|
+
return _jsx(PsImageViewer, { ...props });
|
|
13
|
+
case PsFileKind.pdf:
|
|
14
|
+
return _jsx(PsPdfViewer, { ...props });
|
|
15
|
+
case PsFileKind.csv:
|
|
16
|
+
case PsFileKind.text:
|
|
17
|
+
return _jsx(PsTextViewer, { ...props });
|
|
18
|
+
case PsFileKind.audio:
|
|
19
|
+
case PsFileKind.video:
|
|
20
|
+
return _jsx(PsMediaViewer, { ...props });
|
|
21
|
+
default:
|
|
22
|
+
return _jsx(PsUnsupportedViewer, { ...props });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
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';
|
|
4
|
+
import { PallasoftClassNameService, PallasoftZIndex } from '@pallasoft/ps-design-system';
|
|
5
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
7
|
+
import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
|
|
8
|
+
import { PsFileViewerKinds } from '../constants/ps-file-viewer-kinds';
|
|
9
|
+
import { PsFileViewerLabels } from '../constants/ps-file-viewer-labels';
|
|
10
|
+
import { PsFileSizeService } from '../services/ps-file-size-service';
|
|
11
|
+
import { PsFileTypeService } from '../services/ps-file-type-service';
|
|
12
|
+
import { PsFileDeleteConfirm } from './ps-file-delete-confirm';
|
|
13
|
+
import { PsFileViewerButton } from './ps-file-viewer-button';
|
|
14
|
+
import { PsFileViewerContent } from './ps-file-viewer-content';
|
|
15
|
+
export const PsFileViewer = ({ files, index, isOpen, labels, onClose, onDelete, onIndexChange, pdfWorkerSrc, }) => {
|
|
16
|
+
const text = { ...PsFileViewerLabels, ...labels };
|
|
17
|
+
const { isClosing, shouldRender } = usePsExitTransition(isOpen);
|
|
18
|
+
const zIndex = usePsLayerZIndex(PallasoftZIndex.Modal);
|
|
19
|
+
const [zoom, setZoom] = useState(1);
|
|
20
|
+
const [rotation, setRotation] = useState(0);
|
|
21
|
+
const [search, setSearch] = useState('');
|
|
22
|
+
const [activeMatch, setActiveMatch] = useState(0);
|
|
23
|
+
const [matchCount, setMatchCount] = useState(0);
|
|
24
|
+
const [isConfirming, setIsConfirming] = useState(false);
|
|
25
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
26
|
+
const searchInput = useRef(null);
|
|
27
|
+
const query = usePsDebouncedValue(search, PsFileViewerConfig.searchDebounceMs);
|
|
28
|
+
const file = files.length === 0 ? null : files[Math.min(index, files.length - 1)];
|
|
29
|
+
const kind = file === null ? null : PsFileTypeService.kindOf(file.contentType, file.name);
|
|
30
|
+
const canSearch = kind !== null && PsFileViewerKinds.searchable.includes(kind);
|
|
31
|
+
const canZoom = kind !== null && PsFileViewerKinds.zoomable.includes(kind);
|
|
32
|
+
const canRotate = kind !== null && PsFileViewerKinds.rotatable.includes(kind);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setZoom(1);
|
|
35
|
+
setRotation(0);
|
|
36
|
+
setSearch('');
|
|
37
|
+
setActiveMatch(0);
|
|
38
|
+
setMatchCount(0);
|
|
39
|
+
}, [file === null ? null : file.id]);
|
|
40
|
+
useEffect(() => setActiveMatch(0), [query]);
|
|
41
|
+
const go = (step) => {
|
|
42
|
+
if (files.length > 1)
|
|
43
|
+
onIndexChange((index + step + files.length) % files.length);
|
|
44
|
+
};
|
|
45
|
+
const stepMatch = (step) => {
|
|
46
|
+
if (matchCount > 0)
|
|
47
|
+
setActiveMatch((current) => (current + step + matchCount) % matchCount);
|
|
48
|
+
};
|
|
49
|
+
const changeZoom = (step) => setZoom((current) => Math.min(PsFileViewerConfig.maxZoom, Math.max(PsFileViewerConfig.minZoom, current + step)));
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!isOpen)
|
|
52
|
+
return;
|
|
53
|
+
const onKey = (event) => {
|
|
54
|
+
const isTyping = event.target instanceof HTMLInputElement;
|
|
55
|
+
if (event.key === 'Escape' && !isConfirming)
|
|
56
|
+
onClose();
|
|
57
|
+
else if ((event.metaKey || event.ctrlKey) && event.key === 'f' && canSearch) {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
searchInput.current?.focus();
|
|
60
|
+
}
|
|
61
|
+
else if (isTyping)
|
|
62
|
+
return;
|
|
63
|
+
else if (event.key === 'ArrowRight')
|
|
64
|
+
go(1);
|
|
65
|
+
else if (event.key === 'ArrowLeft')
|
|
66
|
+
go(-1);
|
|
67
|
+
else if (event.key === '+' || event.key === '=')
|
|
68
|
+
changeZoom(PsFileViewerConfig.zoomStep);
|
|
69
|
+
else if (event.key === '-')
|
|
70
|
+
changeZoom(-PsFileViewerConfig.zoomStep);
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener('keydown', onKey);
|
|
73
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
74
|
+
}, [isOpen, isConfirming, canSearch, index, files.length]);
|
|
75
|
+
if (!shouldRender || file === null || typeof document === 'undefined')
|
|
76
|
+
return null;
|
|
77
|
+
const confirmDelete = async () => {
|
|
78
|
+
if (!onDelete)
|
|
79
|
+
return;
|
|
80
|
+
setIsDeleting(true);
|
|
81
|
+
try {
|
|
82
|
+
await onDelete(file);
|
|
83
|
+
setIsConfirming(false);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
setIsDeleting(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return createPortal(_jsx("div", { "aria-label": file.name, "aria-modal": 'true', className: PallasoftClassNameService.merge('fixed inset-0 flex flex-col bg-slate-950/95 text-white backdrop-blur-md', isClosing ? 'animate-ps-fade-out' : 'animate-ps-fade-in'), role: 'dialog', style: { zIndex }, children: _jsxs(PsLayer, { zIndex: zIndex, children: [_jsxs("header", { className: 'flex shrink-0 flex-wrap items-center gap-2 border-b border-white/10 px-3 py-2 sm:px-4', children: [_jsx("div", { className: 'flex min-w-0 flex-1 items-center gap-3', children: _jsxs("div", { className: 'min-w-0', children: [_jsx("p", { className: 'truncate text-sm font-semibold', children: file.name }), _jsx("p", { className: 'truncate text-xs text-white/55', children: [
|
|
90
|
+
files.length > 1 ? `${index + 1} / ${files.length}` : null,
|
|
91
|
+
file.sizeBytes === undefined ? null : PsFileSizeService.format(file.sizeBytes),
|
|
92
|
+
]
|
|
93
|
+
.filter((value) => value !== null)
|
|
94
|
+
.join(' · ') })] }) }), canSearch ? (_jsxs("div", { className: 'order-last flex w-full items-center gap-1 rounded-full 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-9 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: 'h-4 w-4 -rotate-90', iconKey: PsIconKey.ChevronRight }) }), _jsx(PsFileViewerButton, { disabled: matchCount === 0, label: text.nextMatch, onClick: () => stepMatch(1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4 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: 'h-4 w-4', 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: 'h-4 w-4', iconKey: PsIconKey.ZoomIn }) })] })) : null, canRotate ? (_jsx(PsFileViewerButton, { label: text.rotate, onClick: () => setRotation((current) => (current + PsFileViewerConfig.rotateStep) % 360), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Rotate }) })) : null, _jsx("a", { "aria-label": text.download, className: 'ps-focus-ring inline-flex h-9 w-9 items-center justify-center rounded-full 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: 'h-4 w-4', iconKey: PsIconKey.Download }) }), onDelete ? (_jsx(PsFileViewerButton, { label: text.delete, onClick: () => setIsConfirming(true), tone: 'danger', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Trash }) })) : null, _jsx(PsFileViewerButton, { label: text.close, onClick: onClose, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-5 w-5', 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-10 h-11 w-11 -translate-y-1/2 bg-black/40', label: text.previous, onClick: () => go(-1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-5 w-5 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-10 h-11 w-11 -translate-y-1/2 bg-black/40', label: text.next, onClick: () => go(1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-5 w-5', iconKey: PsIconKey.ChevronRight }) })) : null] }), _jsx(PsFileDeleteConfirm, { fileName: file.name, isDeleting: isDeleting, isOpen: isConfirming, labels: text, onCancel: () => setIsConfirming(false), onConfirm: () => void confirmDelete() })] }) }), document.body);
|
|
98
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
const toneClassNames = {
|
|
4
|
+
danger: 'bg-danger',
|
|
5
|
+
primary: 'bg-primary',
|
|
6
|
+
success: 'bg-success',
|
|
7
|
+
};
|
|
8
|
+
export const PsProgressBar = ({ className, label, tone = 'primary', value, }) => {
|
|
9
|
+
const percent = Math.round(Math.min(1, Math.max(0, value)) * 100);
|
|
10
|
+
return (_jsx("div", { "aria-label": label, "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": percent, className: PallasoftClassNameService.merge('h-1.5 w-full overflow-hidden rounded-full bg-line/70', className), role: 'progressbar', children: _jsx("div", { className: PallasoftClassNameService.merge('h-full rounded-full transition-[width] duration-200 ease-out', toneClassNames[tone]), style: { width: `${percent}%` } }) }));
|
|
11
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsButton } from '@pallasoft/ps-core-components';
|
|
3
|
+
import { PsModal } from '@pallasoft/ps-popovers';
|
|
4
|
+
import { PsUploadLabels } from '../constants/ps-upload-labels';
|
|
5
|
+
import { PsUploadQueueService } from '../services/ps-upload-queue-service';
|
|
6
|
+
import { PsProgressBar } from './ps-progress-bar';
|
|
7
|
+
import { PsUploadRow } from './ps-upload-row';
|
|
8
|
+
export const PsUploadModal = ({ isOpen, labels, onClose, queue }) => {
|
|
9
|
+
const text = { ...PsUploadLabels, ...labels };
|
|
10
|
+
const summary = PsUploadQueueService.summary(queue.items);
|
|
11
|
+
const overall = queue.items.length === 0
|
|
12
|
+
? 0
|
|
13
|
+
: queue.items.reduce((total, item) => total + item.progress, 0) / queue.items.length;
|
|
14
|
+
return (_jsx(PsModal, { eyebrow: `${summary.done} / ${summary.total}`, isOpen: isOpen, onClose: onClose, size: 'md', title: queue.isBusy ? text.title : text.titleDone, children: _jsxs("div", { className: 'flex flex-col gap-4', children: [_jsx(PsProgressBar, { label: text.title, tone: summary.failed > 0 ? 'danger' : queue.isBusy ? 'primary' : 'success', value: overall }), _jsx("ul", { className: 'flex max-h-[50vh] flex-col divide-y divide-line/70 overflow-y-auto', children: queue.items.map((item) => (_jsx(PsUploadRow, { item: item, labels: text, onCancel: () => queue.cancel(item.id), onRemove: () => queue.remove(item.id), onRetry: () => queue.retry(item.id) }, item.id))) }), _jsx("div", { className: 'flex justify-end', children: _jsx(PsButton, { onClick: onClose, type: 'button', variant: queue.isBusy ? 'ghost' : 'primary', children: text.close }) })] }) }));
|
|
15
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsButton } from '@pallasoft/ps-core-components';
|
|
3
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
4
|
+
import { PsUploadStatus } from '../constants/ps-upload-status';
|
|
5
|
+
import { PsUploadStatusTones } from '../constants/ps-upload-status-tones';
|
|
6
|
+
import { PsFileSizeService } from '../services/ps-file-size-service';
|
|
7
|
+
import { PsUploadQueueService } from '../services/ps-upload-queue-service';
|
|
8
|
+
import { PsFileKindIcon } from './ps-file-kind-icon';
|
|
9
|
+
import { PsProgressBar } from './ps-progress-bar';
|
|
10
|
+
export const PsUploadRow = ({ item, labels, onCancel, onRemove, onRetry }) => {
|
|
11
|
+
const isActive = PsUploadQueueService.isActive(item);
|
|
12
|
+
const canRetry = item.status === PsUploadStatus.failed || item.status === PsUploadStatus.cancelled;
|
|
13
|
+
const percent = Math.round(item.progress * 100);
|
|
14
|
+
const statusText = item.status === PsUploadStatus.uploading
|
|
15
|
+
? `${labels.uploading} ${percent}%`
|
|
16
|
+
: item.status === PsUploadStatus.failed && item.error
|
|
17
|
+
? item.error
|
|
18
|
+
: labels[item.status];
|
|
19
|
+
return (_jsxs("li", { className: 'flex items-center gap-3 py-3', 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: [_jsxs("div", { className: 'flex items-baseline justify-between gap-3', children: [_jsx("p", { className: 'truncate text-sm font-medium text-ink', children: item.file.name }), _jsx("span", { className: 'shrink-0 text-xs text-ink/55', children: PsFileSizeService.format(item.file.size) })] }), _jsx(PsProgressBar, { label: item.file.name, tone: PsUploadStatusTones[item.status], value: item.status === PsUploadStatus.failed ? 1 : item.progress }), _jsxs("p", { className: item.status === PsUploadStatus.failed || item.status === PsUploadStatus.cancelled
|
|
20
|
+
? 'text-xs text-danger'
|
|
21
|
+
: item.status === PsUploadStatus.done
|
|
22
|
+
? 'flex items-center gap-1 text-xs text-success'
|
|
23
|
+
: 'text-xs text-ink/60', role: item.status === PsUploadStatus.failed ? 'alert' : undefined, children: [item.status === PsUploadStatus.done ? (_jsx(PsIcon, { "aria-hidden": 'true', className: 'h-3.5 w-3.5', iconKey: PsIconKey.Check })) : null, statusText] })] }), _jsxs("div", { className: 'flex shrink-0 items-center gap-1', children: [canRetry ? (_jsx(PsButton, { leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Refresh }), onClick: onRetry, size: 'compact', type: 'button', variant: 'ghost', children: labels.retry })) : null, _jsx(PsButton, { "aria-label": `${isActive ? labels.cancel : labels.remove} ${item.file.name}`, onClick: isActive ? onCancel : onRemove, size: 'compact', type: 'button', variant: 'ghost', children: isActive ? labels.cancel : labels.remove })] })] }));
|
|
24
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
export const PsHighlightedText = ({ activeMatch, segments }) => (_jsx(_Fragment, { children: segments.map((segment, index) => segment.isMatch ? (_jsx("mark", { className: segment.matchIndex === activeMatch
|
|
3
|
+
? 'rounded-sm bg-warning px-0.5 text-ink ring-2 ring-warning'
|
|
4
|
+
: 'rounded-sm bg-warning/40 px-0.5 text-inherit', "data-ps-match": segment.matchIndex === null ? undefined : segment.matchIndex, children: segment.text }, index)) : (_jsx("span", { children: segment.text }, index))) }));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
export const PsImageViewer = ({ file, labels, rotation, zoom }) => {
|
|
4
|
+
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
5
|
+
const [hasFailed, setHasFailed] = useState(false);
|
|
6
|
+
const drag = useRef(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
setOffset({ x: 0, y: 0 });
|
|
9
|
+
setHasFailed(false);
|
|
10
|
+
}, [file.id, zoom === 1]);
|
|
11
|
+
const onPointerDown = (event) => {
|
|
12
|
+
if (zoom <= 1)
|
|
13
|
+
return;
|
|
14
|
+
drag.current = { x: event.clientX - offset.x, y: event.clientY - offset.y };
|
|
15
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
16
|
+
};
|
|
17
|
+
const onPointerMove = (event) => {
|
|
18
|
+
if (drag.current === null)
|
|
19
|
+
return;
|
|
20
|
+
setOffset({ x: event.clientX - drag.current.x, y: event.clientY - drag.current.y });
|
|
21
|
+
};
|
|
22
|
+
if (hasFailed)
|
|
23
|
+
return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
|
|
24
|
+
return (_jsx("div", { className: `flex h-full w-full touch-none items-center justify-center overflow-hidden ${zoom > 1 ? 'cursor-grab active:cursor-grabbing' : ''}`, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: () => {
|
|
25
|
+
drag.current = null;
|
|
26
|
+
}, 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: {
|
|
27
|
+
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
|
|
28
|
+
} }) }));
|
|
29
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PsFileKind } from '../../constants/ps-file-kind';
|
|
3
|
+
import { PsFileTypeService } from '../../services/ps-file-type-service';
|
|
4
|
+
export const PsMediaViewer = ({ file }) => PsFileTypeService.kindOf(file.contentType, file.name) === PsFileKind.video ? (_jsx("video", { className: 'm-auto max-h-full max-w-full rounded-lg shadow-2xl', controls: true, src: file.url })) : (_jsx("audio", { className: 'm-auto w-full max-w-xl', controls: true, src: file.url }));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { usePsInView } from '../../hooks/use-ps-in-view';
|
|
4
|
+
import { PsPdfService } from '../../services/ps-pdf-service';
|
|
5
|
+
export const PsPdfPage = ({ activeMatch, document, label, matches, pageNumber, rotation, scale, }) => {
|
|
6
|
+
const canvas = useRef(null);
|
|
7
|
+
const [container, isInView] = usePsInView('600px');
|
|
8
|
+
const [size, setSize] = useState(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let isCurrent = true;
|
|
11
|
+
let task = null;
|
|
12
|
+
document.getPage(pageNumber).then((page) => {
|
|
13
|
+
if (!isCurrent)
|
|
14
|
+
return;
|
|
15
|
+
const viewport = page.getViewport({ rotation: page.rotate + rotation, scale });
|
|
16
|
+
setSize({ height: viewport.height, width: viewport.width });
|
|
17
|
+
if (isInView && canvas.current)
|
|
18
|
+
task = PsPdfService.render(page, canvas.current, scale, rotation);
|
|
19
|
+
});
|
|
20
|
+
return () => {
|
|
21
|
+
isCurrent = false;
|
|
22
|
+
task?.cancel();
|
|
23
|
+
};
|
|
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
|
|
26
|
+
? 'pointer-events-none absolute rounded-sm bg-warning/60 ring-2 ring-warning'
|
|
27
|
+
: 'pointer-events-none absolute rounded-sm bg-warning/35', style: {
|
|
28
|
+
height: `${match.height * 100}%`,
|
|
29
|
+
left: `${match.left * 100}%`,
|
|
30
|
+
top: `${match.top * 100}%`,
|
|
31
|
+
width: `${match.width * 100}%`,
|
|
32
|
+
} }, index)))] }));
|
|
33
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { PsFileViewerConfig } from '../../constants/ps-file-viewer-config';
|
|
4
|
+
import { usePsInView } from '../../hooks/use-ps-in-view';
|
|
5
|
+
import { PsPdfService } from '../../services/ps-pdf-service';
|
|
6
|
+
export const PsPdfThumbnail = ({ document, hasMatches, isCurrent, label, onSelect, pageNumber, rotation, }) => {
|
|
7
|
+
const canvas = useRef(null);
|
|
8
|
+
const [container, isInView] = usePsInView('200px');
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!isInView)
|
|
11
|
+
return;
|
|
12
|
+
let isCurrent = true;
|
|
13
|
+
let task = null;
|
|
14
|
+
document.getPage(pageNumber).then((page) => {
|
|
15
|
+
if (isCurrent && canvas.current)
|
|
16
|
+
task = PsPdfService.render(page, canvas.current, PsFileViewerConfig.pdfThumbnailScale, rotation);
|
|
17
|
+
});
|
|
18
|
+
return () => {
|
|
19
|
+
isCurrent = false;
|
|
20
|
+
task?.cancel();
|
|
21
|
+
};
|
|
22
|
+
}, [document, pageNumber, rotation, isInView]);
|
|
23
|
+
return (_jsxs("button", { "aria-current": isCurrent ? 'page' : undefined, "aria-label": `${label} ${pageNumber}`, className: `ps-focus-ring relative flex flex-col items-center gap-1 rounded-md p-1.5 transition ${isCurrent ? 'bg-white/15' : 'hover:bg-white/10'}`, onClick: onSelect, ref: container, type: 'button', children: [_jsx("canvas", { className: `block min-h-16 min-w-12 bg-white ${isCurrent ? 'ring-2 ring-primary' : ''}`, ref: canvas }), _jsx("span", { className: 'text-[0.7rem] text-white/70', children: pageNumber }), hasMatches ? (_jsx("span", { className: 'absolute right-1 top-1 h-2 w-2 rounded-full bg-warning' })) : null] }));
|
|
24
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { PsSpinner, usePsElementSize } from '@pallasoft/ps-core-components';
|
|
4
|
+
import { PsFileViewerConfig } from '../../constants/ps-file-viewer-config';
|
|
5
|
+
import { PsPdfService } from '../../services/ps-pdf-service';
|
|
6
|
+
import { PsPdfPage } from './ps-pdf-page';
|
|
7
|
+
import { PsPdfThumbnail } from './ps-pdf-thumbnail';
|
|
8
|
+
export const PsPdfViewer = ({ activeMatch, file, labels, onMatchCount, pdfWorkerSrc, query, rotation, zoom, }) => {
|
|
9
|
+
const [document, setDocument] = useState(null);
|
|
10
|
+
const [hasFailed, setHasFailed] = useState(false);
|
|
11
|
+
const [pageWidth, setPageWidth] = useState(0);
|
|
12
|
+
const [matches, setMatches] = useState([]);
|
|
13
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
14
|
+
const [frame, frameSize] = usePsElementSize();
|
|
15
|
+
const scroller = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let isCurrent = true;
|
|
18
|
+
let opened = null;
|
|
19
|
+
setDocument(null);
|
|
20
|
+
setHasFailed(false);
|
|
21
|
+
if (pdfWorkerSrc === undefined) {
|
|
22
|
+
setHasFailed(true);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
PsPdfService.open(file.url, pdfWorkerSrc)
|
|
26
|
+
.then(async (loaded) => {
|
|
27
|
+
opened = loaded;
|
|
28
|
+
const firstPage = await loaded.getPage(1);
|
|
29
|
+
if (!isCurrent)
|
|
30
|
+
return;
|
|
31
|
+
setPageWidth(firstPage.getViewport({ rotation: firstPage.rotate + rotation, scale: 1 }).width);
|
|
32
|
+
setDocument(loaded);
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {
|
|
35
|
+
if (isCurrent)
|
|
36
|
+
setHasFailed(true);
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
isCurrent = false;
|
|
40
|
+
void opened?.loadingTask.destroy();
|
|
41
|
+
};
|
|
42
|
+
}, [file.url, pdfWorkerSrc]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (document === null)
|
|
45
|
+
return;
|
|
46
|
+
let isCurrent = true;
|
|
47
|
+
document.getPage(1).then((page) => {
|
|
48
|
+
if (isCurrent)
|
|
49
|
+
setPageWidth(page.getViewport({ rotation: page.rotate + rotation, scale: 1 }).width);
|
|
50
|
+
});
|
|
51
|
+
return () => {
|
|
52
|
+
isCurrent = false;
|
|
53
|
+
};
|
|
54
|
+
}, [document, rotation]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (document === null || query.trim().length === 0) {
|
|
57
|
+
setMatches([]);
|
|
58
|
+
onMatchCount(0);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let isCurrent = true;
|
|
62
|
+
PsPdfService.search(document, query, rotation).then((found) => {
|
|
63
|
+
if (!isCurrent)
|
|
64
|
+
return;
|
|
65
|
+
setMatches(found);
|
|
66
|
+
onMatchCount(found.length);
|
|
67
|
+
});
|
|
68
|
+
return () => {
|
|
69
|
+
isCurrent = false;
|
|
70
|
+
};
|
|
71
|
+
}, [document, query, rotation]);
|
|
72
|
+
const active = matches.length > 0 ? matches[activeMatch % matches.length] : null;
|
|
73
|
+
const scrollToPage = (pageNumber) => {
|
|
74
|
+
const node = scroller.current?.querySelector(`[data-ps-pdf-page='${pageNumber}']`);
|
|
75
|
+
if (node instanceof HTMLElement)
|
|
76
|
+
node.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
77
|
+
setCurrentPage(pageNumber);
|
|
78
|
+
};
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (active !== null)
|
|
81
|
+
scrollToPage(active.pageNumber);
|
|
82
|
+
}, [active]);
|
|
83
|
+
const scale = useMemo(() => {
|
|
84
|
+
const fit = frameSize.width > 0 && pageWidth > 0
|
|
85
|
+
? (frameSize.width - 48) / pageWidth
|
|
86
|
+
: PsFileViewerConfig.pdfBaseScale;
|
|
87
|
+
return Math.max(0.1, fit * zoom);
|
|
88
|
+
}, [frameSize.width, pageWidth, zoom]);
|
|
89
|
+
const pages = document === null ? [] : Array.from({ length: document.numPages }, (_, index) => index + 1);
|
|
90
|
+
return (_jsxs("div", { className: 'flex h-full w-full min-w-0', children: [document !== null && document.numPages > 1 ? (_jsx("nav", { className: 'ps-scrollbar-hidden hidden w-24 shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-white/10 py-3 md:flex', children: pages.map((pageNumber) => (_jsx(PsPdfThumbnail, { document: document, hasMatches: matches.some((match) => match.pageNumber === pageNumber), isCurrent: pageNumber === currentPage, label: labels.page, onSelect: () => scrollToPage(pageNumber), pageNumber: pageNumber, rotation: rotation }, pageNumber))) })) : null, _jsx("div", { className: 'min-w-0 flex-1', ref: frame, children: hasFailed ? (_jsx("p", { className: 'flex h-full items-center justify-center text-sm text-white/70', children: labels.loadFailed })) : document === null ? (_jsx("div", { className: 'flex h-full items-center justify-center text-white', children: _jsx(PsSpinner, { color: 'current', size: 'large' }) })) : (_jsx("div", { className: 'flex h-full flex-col items-center gap-4 overflow-auto px-6 py-6', onScroll: (event) => {
|
|
91
|
+
const top = event.currentTarget.scrollTop + event.currentTarget.clientHeight / 3;
|
|
92
|
+
const visible = Array.from(event.currentTarget.querySelectorAll('[data-ps-pdf-page]')).find((node) => node.offsetTop + node.offsetHeight > top);
|
|
93
|
+
if (visible)
|
|
94
|
+
setCurrentPage(Number(visible.dataset.psPdfPage));
|
|
95
|
+
}, ref: scroller, children: pages.map((pageNumber) => (_jsx(PsPdfPage, { activeMatch: active, document: document, label: labels.page, matches: matches.filter((match) => match.pageNumber === pageNumber), pageNumber: pageNumber, rotation: rotation, scale: scale }, pageNumber))) })) })] }));
|
|
96
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { PsSpinner } from '@pallasoft/ps-core-components';
|
|
4
|
+
import { PsFileKind } from '../../constants/ps-file-kind';
|
|
5
|
+
import { usePsActiveMatchScroll } from '../../hooks/use-ps-active-match-scroll';
|
|
6
|
+
import { usePsFileText } from '../../hooks/use-ps-file-text';
|
|
7
|
+
import { PsCsvService } from '../../services/ps-csv-service';
|
|
8
|
+
import { PsFileTypeService } from '../../services/ps-file-type-service';
|
|
9
|
+
import { PsTextSearchService } from '../../services/ps-text-search-service';
|
|
10
|
+
import { PsHighlightedText } from './ps-highlighted-text';
|
|
11
|
+
export const PsTextViewer = ({ activeMatch, file, labels, onMatchCount, query, zoom, }) => {
|
|
12
|
+
const { hasFailed, text } = usePsFileText(file.url);
|
|
13
|
+
const container = useRef(null);
|
|
14
|
+
const isCsv = PsFileTypeService.kindOf(file.contentType, file.name) === PsFileKind.csv;
|
|
15
|
+
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
|
+
const segments = useMemo(() => isCsv || text === null
|
|
26
|
+
? []
|
|
27
|
+
: 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]);
|
|
32
|
+
usePsActiveMatchScroll(container, activeMatch, query);
|
|
33
|
+
if (hasFailed)
|
|
34
|
+
return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
|
|
35
|
+
if (text === null)
|
|
36
|
+
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 }) })) }) }));
|
|
38
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
3
|
+
import { PsFileKindIcon } from '../ps-file-kind-icon';
|
|
4
|
+
export const PsUnsupportedViewer = ({ file, labels }) => (_jsxs("div", { className: 'm-auto flex max-w-sm flex-col items-center gap-4 rounded-2xl bg-surface p-8 text-center shadow-2xl', children: [_jsx(PsFileKindIcon, { className: 'h-14 w-14', contentType: file.contentType, name: file.name }), _jsx("p", { className: 'break-all font-semibold text-ink', children: file.name }), _jsx("p", { className: 'text-sm text-ink/60', children: labels.unsupported }), _jsxs("a", { className: 'ps-focus-ring inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-white hover:bg-primary/90', download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Download }), labels.download] })] }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PsIconKey } from '@pallasoft/ps-icons';
|
|
2
|
+
import { PsFileKind } from './ps-file-kind';
|
|
3
|
+
export const PsFileKindIcons = {
|
|
4
|
+
[PsFileKind.audio]: PsIconKey.Audio,
|
|
5
|
+
[PsFileKind.csv]: PsIconKey.Sheet,
|
|
6
|
+
[PsFileKind.image]: PsIconKey.Image,
|
|
7
|
+
[PsFileKind.other]: PsIconKey.File,
|
|
8
|
+
[PsFileKind.pdf]: PsIconKey.Doc,
|
|
9
|
+
[PsFileKind.text]: PsIconKey.Text,
|
|
10
|
+
[PsFileKind.video]: PsIconKey.Video,
|
|
11
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PsFileKind } from './ps-file-kind';
|
|
2
|
+
export const PsFileTypeConfig = {
|
|
3
|
+
csvTypes: ['text/csv', 'application/csv', 'application/vnd.ms-excel'],
|
|
4
|
+
extensionKinds: {
|
|
5
|
+
csv: PsFileKind.csv,
|
|
6
|
+
gif: PsFileKind.image,
|
|
7
|
+
heic: PsFileKind.image,
|
|
8
|
+
jpeg: PsFileKind.image,
|
|
9
|
+
jpg: PsFileKind.image,
|
|
10
|
+
json: PsFileKind.text,
|
|
11
|
+
log: PsFileKind.text,
|
|
12
|
+
md: PsFileKind.text,
|
|
13
|
+
mov: PsFileKind.video,
|
|
14
|
+
mp3: PsFileKind.audio,
|
|
15
|
+
mp4: PsFileKind.video,
|
|
16
|
+
pdf: PsFileKind.pdf,
|
|
17
|
+
png: PsFileKind.image,
|
|
18
|
+
svg: PsFileKind.image,
|
|
19
|
+
txt: PsFileKind.text,
|
|
20
|
+
wav: PsFileKind.audio,
|
|
21
|
+
webm: PsFileKind.video,
|
|
22
|
+
webp: PsFileKind.image,
|
|
23
|
+
xml: PsFileKind.text,
|
|
24
|
+
},
|
|
25
|
+
sizeUnits: ['B', 'KB', 'MB', 'GB'],
|
|
26
|
+
textTypes: ['application/json', 'application/xml'],
|
|
27
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PsFileKind } from './ps-file-kind';
|
|
2
|
+
export const PsFileViewerKinds = {
|
|
3
|
+
rotatable: [PsFileKind.image, PsFileKind.pdf],
|
|
4
|
+
searchable: [PsFileKind.csv, PsFileKind.pdf, PsFileKind.text],
|
|
5
|
+
zoomable: [PsFileKind.csv, PsFileKind.image, PsFileKind.pdf, PsFileKind.text],
|
|
6
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const PsFileViewerLabels = {
|
|
2
|
+
close: 'Close',
|
|
3
|
+
delete: 'Delete',
|
|
4
|
+
deleteConfirm: 'Delete file',
|
|
5
|
+
deleteConfirmBody: 'This file will be removed. This cannot be undone from here.',
|
|
6
|
+
deleteCancel: 'Keep file',
|
|
7
|
+
download: 'Download',
|
|
8
|
+
loadFailed: 'This file could not be opened.',
|
|
9
|
+
next: 'Next file',
|
|
10
|
+
nextMatch: 'Next match',
|
|
11
|
+
noMatches: 'No matches',
|
|
12
|
+
page: 'Page',
|
|
13
|
+
previous: 'Previous file',
|
|
14
|
+
previousMatch: 'Previous match',
|
|
15
|
+
rotate: 'Rotate',
|
|
16
|
+
search: 'Search in file',
|
|
17
|
+
unsupported: 'This file type cannot be previewed here. Download it to open it.',
|
|
18
|
+
zoomIn: 'Zoom in',
|
|
19
|
+
zoomOut: 'Zoom out',
|
|
20
|
+
zoomReset: 'Fit',
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const PsUploadLabels = {
|
|
2
|
+
cancel: 'Cancel',
|
|
3
|
+
cancelled: 'Cancelled',
|
|
4
|
+
close: 'Close',
|
|
5
|
+
done: 'Uploaded',
|
|
6
|
+
failed: 'Failed',
|
|
7
|
+
processing: 'Saving',
|
|
8
|
+
remove: 'Remove',
|
|
9
|
+
retry: 'Retry',
|
|
10
|
+
title: 'Uploading files',
|
|
11
|
+
titleDone: 'Upload finished',
|
|
12
|
+
uploading: 'Uploading',
|
|
13
|
+
waiting: 'Waiting',
|
|
14
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PsUploadStatus } from './ps-upload-status';
|
|
2
|
+
export const PsUploadStatusTones = {
|
|
3
|
+
[PsUploadStatus.cancelled]: 'danger',
|
|
4
|
+
[PsUploadStatus.done]: 'success',
|
|
5
|
+
[PsUploadStatus.failed]: 'danger',
|
|
6
|
+
[PsUploadStatus.processing]: 'primary',
|
|
7
|
+
[PsUploadStatus.uploading]: 'primary',
|
|
8
|
+
[PsUploadStatus.waiting]: 'primary',
|
|
9
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
export const usePsActiveMatchScroll = (container, activeMatch, dependency) => {
|
|
3
|
+
useEffect(() => {
|
|
4
|
+
const node = container.current?.querySelector(`[data-ps-match='${activeMatch}']`);
|
|
5
|
+
if (node instanceof HTMLElement)
|
|
6
|
+
node.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
7
|
+
}, [activeMatch, dependency]);
|
|
8
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
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;
|
|
21
|
+
};
|