@pallasoft/ps-mobile-components 1.11.0 → 1.12.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/dist/components/ps-document-highlight-layer/index.d.ts +2 -0
- package/dist/components/ps-document-highlight-layer/index.js +2 -0
- package/dist/components/ps-document-highlight-layer/ps-document-highlight-layer-props.d.ts +8 -0
- package/dist/components/ps-document-highlight-layer/ps-document-highlight-layer-props.js +1 -0
- package/dist/components/ps-document-highlight-layer/ps-document-highlight-layer.d.ts +2 -0
- package/dist/components/ps-document-highlight-layer/ps-document-highlight-layer.js +19 -0
- package/dist/components/ps-document-page/ps-document-page-props.d.ts +2 -0
- package/dist/components/ps-document-page/ps-document-page.d.ts +1 -1
- package/dist/components/ps-document-page/ps-document-page.js +3 -3
- package/dist/components/ps-document-search-bar/index.d.ts +2 -0
- package/dist/components/ps-document-search-bar/index.js +2 -0
- package/dist/components/ps-document-search-bar/ps-document-search-bar-props.d.ts +11 -0
- package/dist/components/ps-document-search-bar/ps-document-search-bar-props.js +1 -0
- package/dist/components/ps-document-search-bar/ps-document-search-bar.d.ts +2 -0
- package/dist/components/ps-document-search-bar/ps-document-search-bar.js +46 -0
- package/dist/components/ps-document-search-step/index.d.ts +2 -0
- package/dist/components/ps-document-search-step/index.js +2 -0
- package/dist/components/ps-document-search-step/ps-document-search-step-props.d.ts +7 -0
- package/dist/components/ps-document-search-step/ps-document-search-step-props.js +1 -0
- package/dist/components/ps-document-search-step/ps-document-search-step.d.ts +2 -0
- package/dist/components/ps-document-search-step/ps-document-search-step.js +30 -0
- package/dist/components/ps-document-text-page/ps-document-text-page-props.d.ts +2 -0
- package/dist/components/ps-document-text-page/ps-document-text-page.d.ts +1 -1
- package/dist/components/ps-document-text-page/ps-document-text-page.js +27 -3
- package/dist/components/ps-document-viewer/ps-document-viewer.js +51 -2
- package/dist/components/ps-document-zoom-image/ps-document-zoom-image-props.d.ts +4 -0
- package/dist/components/ps-document-zoom-image/ps-document-zoom-image.d.ts +1 -1
- package/dist/components/ps-document-zoom-image/ps-document-zoom-image.js +20 -3
- package/dist/components/ps-text/ps-text-props.d.ts +1 -0
- package/dist/components/ps-text/ps-text.d.ts +1 -1
- package/dist/components/ps-text/ps-text.js +2 -2
- package/dist/constants/ps-document-search-config.d.ts +19 -0
- package/dist/constants/ps-document-search-config.js +19 -0
- package/dist/hooks/use-ps-document-search-texts.d.ts +2 -0
- package/dist/hooks/use-ps-document-search-texts.js +26 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/interfaces/ps-document-highlight-rect.d.ts +5 -0
- package/dist/interfaces/ps-document-highlight-rect.js +1 -0
- package/dist/interfaces/ps-document-item.d.ts +3 -0
- package/dist/interfaces/ps-document-labels.d.ts +7 -0
- package/dist/interfaces/ps-document-normalized-text.d.ts +5 -0
- package/dist/interfaces/ps-document-normalized-text.js +1 -0
- package/dist/interfaces/ps-document-point.d.ts +4 -0
- package/dist/interfaces/ps-document-point.js +1 -0
- package/dist/interfaces/ps-document-search-labels.d.ts +9 -0
- package/dist/interfaces/ps-document-search-labels.js +1 -0
- package/dist/interfaces/ps-document-search-location.d.ts +4 -0
- package/dist/interfaces/ps-document-search-location.js +1 -0
- package/dist/interfaces/ps-document-search-match.d.ts +4 -0
- package/dist/interfaces/ps-document-search-match.js +1 -0
- package/dist/interfaces/ps-document-search-state.d.ts +4 -0
- package/dist/interfaces/ps-document-search-state.js +1 -0
- package/dist/interfaces/ps-document-text-box.d.ts +6 -0
- package/dist/interfaces/ps-document-text-box.js +1 -0
- package/dist/interfaces/ps-document-text-layer-word.d.ts +5 -0
- package/dist/interfaces/ps-document-text-layer-word.js +1 -0
- package/dist/interfaces/ps-document-text-layer.d.ts +6 -0
- package/dist/interfaces/ps-document-text-layer.js +1 -0
- package/dist/interfaces/ps-document-text-line.d.ts +4 -0
- package/dist/interfaces/ps-document-text-line.js +1 -0
- package/dist/interfaces/ps-document-text-segment.d.ts +4 -0
- package/dist/interfaces/ps-document-text-segment.js +1 -0
- package/dist/interfaces/ps-document-word-match.d.ts +3 -0
- package/dist/interfaces/ps-document-word-match.js +1 -0
- package/dist/services/ps-document-search-service.d.ts +40 -0
- package/dist/services/ps-document-search-service.js +201 -0
- package/dist/services/ps-document-search-style-service.d.ts +7 -0
- package/dist/services/ps-document-search-style-service.js +22 -0
- package/dist/services/ps-document-viewer-entry-service.js +1 -0
- package/dist/services/ps-document-zoom-service.d.ts +1 -0
- package/dist/services/ps-document-zoom-service.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PsDocumentSearchState } from '../../interfaces/ps-document-search-state';
|
|
2
|
+
import type { PsDocumentTextLayer } from '../../interfaces/ps-document-text-layer';
|
|
3
|
+
import type { PsSize } from '../../interfaces/ps-size';
|
|
4
|
+
export interface PsDocumentHighlightLayerProps {
|
|
5
|
+
layer: PsDocumentTextLayer;
|
|
6
|
+
search: PsDocumentSearchState;
|
|
7
|
+
size: PsSize;
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
|
|
4
|
+
import { PsDocumentSearchService } from '../../services/ps-document-search-service';
|
|
5
|
+
import { PsDocumentSearchStyleService } from '../../services/ps-document-search-style-service';
|
|
6
|
+
export const PsDocumentHighlightLayer = ({ layer, search, size, }) => {
|
|
7
|
+
const theme = usePsMobileTheme();
|
|
8
|
+
const rects = PsDocumentSearchService.highlights(layer, search.query, size, search.activeIndex);
|
|
9
|
+
return (_jsx(View, { pointerEvents: 'none', style: { bottom: 0, left: 0, position: 'absolute', right: 0, top: 0 }, testID: 'ps-document-highlight-layer', children: rects.map((rect, index) => (_jsx(View, { style: [
|
|
10
|
+
PsDocumentSearchStyleService.rect(theme, rect.isActive),
|
|
11
|
+
{
|
|
12
|
+
height: rect.box.height,
|
|
13
|
+
left: rect.box.left,
|
|
14
|
+
position: 'absolute',
|
|
15
|
+
top: rect.box.top,
|
|
16
|
+
width: rect.box.width,
|
|
17
|
+
},
|
|
18
|
+
], testID: rect.isActive ? 'ps-document-highlight-active' : 'ps-document-highlight' }, index))) }));
|
|
19
|
+
};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { PsDocumentLoadStatus } from '../../constants/ps-document-load-status';
|
|
2
2
|
import type { PsDocumentItem } from '../../interfaces/ps-document-item';
|
|
3
3
|
import type { PsDocumentLabels } from '../../interfaces/ps-document-labels';
|
|
4
|
+
import type { PsDocumentSearchState } from '../../interfaces/ps-document-search-state';
|
|
4
5
|
export interface PsDocumentPageProps {
|
|
5
6
|
item: PsDocumentItem;
|
|
6
7
|
labels: PsDocumentLabels;
|
|
7
8
|
onDismiss: () => void;
|
|
8
9
|
onRetry?: () => void;
|
|
9
10
|
onZoomChange: (isZoomed: boolean) => void;
|
|
11
|
+
search?: PsDocumentSearchState;
|
|
10
12
|
status?: PsDocumentLoadStatus;
|
|
11
13
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { PsDocumentPageProps } from './ps-document-page-props';
|
|
2
|
-
export declare const PsDocumentPage: ({ item, labels, onDismiss, onRetry, onZoomChange, status, }: PsDocumentPageProps) => import("react").JSX.Element;
|
|
2
|
+
export declare const PsDocumentPage: ({ item, labels, onDismiss, onRetry, onZoomChange, search, status, }: PsDocumentPageProps) => import("react").JSX.Element;
|
|
@@ -9,7 +9,7 @@ import { PsDocumentTextPage } from '../ps-document-text-page/ps-document-text-pa
|
|
|
9
9
|
import { PsDocumentUnsupported } from '../ps-document-unsupported/ps-document-unsupported';
|
|
10
10
|
import { PsDocumentZoomImage } from '../ps-document-zoom-image/ps-document-zoom-image';
|
|
11
11
|
import { PsSpinner } from '../ps-spinner/ps-spinner';
|
|
12
|
-
export const PsDocumentPage = ({ item, labels, onDismiss, onRetry, onZoomChange, status = PsDocumentLoadStatus.Ready, }) => {
|
|
12
|
+
export const PsDocumentPage = ({ item, labels, onDismiss, onRetry, onZoomChange, search, status = PsDocumentLoadStatus.Ready, }) => {
|
|
13
13
|
const [attempt, setAttempt] = useState(0);
|
|
14
14
|
const [hasFailed, setHasFailed] = useState(false);
|
|
15
15
|
if (item.kind === PsDocumentKind.Pdf) {
|
|
@@ -23,9 +23,9 @@ export const PsDocumentPage = ({ item, labels, onDismiss, onRetry, onZoomChange,
|
|
|
23
23
|
return hasFailed ? (_jsx(PsDocumentMessage, { actionLabel: labels.retry, iconKey: PsIconKey.Warning, isOnDark: true, onAction: () => {
|
|
24
24
|
setHasFailed(false);
|
|
25
25
|
setAttempt((current) => current + 1);
|
|
26
|
-
}, title: labels.loadFailed })) : (_jsx(PsDocumentZoomImage, { name: item.name, onDismiss: onDismiss, onError: () => setHasFailed(true), onZoomChange: onZoomChange, source: item.source }, attempt));
|
|
26
|
+
}, title: labels.loadFailed })) : (_jsx(PsDocumentZoomImage, { name: item.name, onDismiss: onDismiss, onError: () => setHasFailed(true), onZoomChange: onZoomChange, search: search, source: item.source, textLayer: item.textLayer }, attempt));
|
|
27
27
|
}
|
|
28
28
|
if (item.kind === PsDocumentKind.Text)
|
|
29
|
-
return _jsx(PsDocumentTextPage, { item: item, labels: labels });
|
|
29
|
+
return _jsx(PsDocumentTextPage, { item: item, labels: labels, search: search });
|
|
30
30
|
return _jsx(PsDocumentUnsupported, { isOnDark: true, item: item, labels: labels });
|
|
31
31
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PsDocumentSearchLabels } from '../../interfaces/ps-document-search-labels';
|
|
2
|
+
export interface PsDocumentSearchBarProps {
|
|
3
|
+
activeIndex: number;
|
|
4
|
+
labels: PsDocumentSearchLabels;
|
|
5
|
+
onChange: (query: string) => void;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onNext: () => void;
|
|
8
|
+
onPrevious: () => void;
|
|
9
|
+
query: string;
|
|
10
|
+
total: number;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsIconKey } from '@pallasoft/ps-icons';
|
|
3
|
+
import { Platform, Pressable, TextInput, View } from 'react-native';
|
|
4
|
+
import { PsDocumentConfig } from '../../constants/ps-document-config';
|
|
5
|
+
import { PsDocumentSearchConfig } from '../../constants/ps-document-search-config';
|
|
6
|
+
import { PsLayoutConfig } from '../../constants/ps-layout-config';
|
|
7
|
+
import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
|
|
8
|
+
import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
|
|
9
|
+
import { PsDocumentSearchService } from '../../services/ps-document-search-service';
|
|
10
|
+
import { PsDocumentSearchStyleService } from '../../services/ps-document-search-style-service';
|
|
11
|
+
import { PsMobileColorService } from '../../services/ps-mobile-color-service';
|
|
12
|
+
import { PsDocumentSearchStep } from '../ps-document-search-step/ps-document-search-step';
|
|
13
|
+
import { PsIconButton } from '../ps-icon-button/ps-icon-button';
|
|
14
|
+
import { PsIcon } from '../ps-icon/ps-icon';
|
|
15
|
+
import { PsText } from '../ps-text/ps-text';
|
|
16
|
+
export const PsDocumentSearchBar = ({ activeIndex, labels, onChange, onClose, onNext, onPrevious, query, total, }) => {
|
|
17
|
+
const theme = usePsMobileTheme();
|
|
18
|
+
const isMaterial = Platform.OS === 'android';
|
|
19
|
+
const hasQuery = PsDocumentSearchService.terms(query).length > 0;
|
|
20
|
+
const status = !hasQuery
|
|
21
|
+
? null
|
|
22
|
+
: total === 0
|
|
23
|
+
? labels.noMatches
|
|
24
|
+
: PsDocumentSearchService.countLabel(labels.matchCount, activeIndex + 1, total);
|
|
25
|
+
return (_jsxs(View, { style: {
|
|
26
|
+
alignItems: 'center',
|
|
27
|
+
flexDirection: 'row',
|
|
28
|
+
gap: PsDocumentSearchConfig.barGap,
|
|
29
|
+
paddingHorizontal: PsDocumentConfig.barPaddingHorizontal,
|
|
30
|
+
paddingVertical: PsDocumentConfig.barPaddingVertical,
|
|
31
|
+
}, testID: 'ps-document-search-bar', children: [_jsxs(View, { style: {
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
backgroundColor: PsMobileColorService.withAlpha(theme.white, PsDocumentSearchConfig.fieldAlpha),
|
|
34
|
+
borderRadius: PsDocumentSearchStyleService.fieldRadius(isMaterial),
|
|
35
|
+
flex: 1,
|
|
36
|
+
flexDirection: 'row',
|
|
37
|
+
gap: PsMobileControlConfig.buttonIconSpacing,
|
|
38
|
+
height: PsMobileControlConfig.controlHeights.compact,
|
|
39
|
+
paddingHorizontal: PsDocumentSearchConfig.fieldPaddingX,
|
|
40
|
+
}, children: [_jsx(PsIcon, { color: theme.white, iconKey: PsIconKey.Search, size: PsMobileControlConfig.iconSizeInline }), _jsx(TextInput, { accessibilityLabel: labels.searchPlaceholder, autoCapitalize: 'none', autoCorrect: false, autoFocus: true, clearButtonMode: 'while-editing', onChangeText: onChange, onSubmitEditing: onNext, placeholder: labels.searchPlaceholder, placeholderTextColor: PsMobileColorService.withAlpha(theme.white, PsDocumentConfig.chromeAlpha), returnKeyType: 'search', selectionColor: theme.primary, style: {
|
|
41
|
+
color: theme.white,
|
|
42
|
+
flex: 1,
|
|
43
|
+
fontSize: PsMobileControlConfig.controlFontSize,
|
|
44
|
+
padding: 0,
|
|
45
|
+
}, testID: 'ps-document-search-input', value: query }), status === null ? null : (_jsx(PsText, { tone: 'white', variant: 'caption', children: status }))] }), _jsx(PsDocumentSearchStep, { isDisabled: total === 0, isUp: true, label: labels.previousMatch, onPress: onPrevious, testID: 'ps-document-search-previous' }), _jsx(PsDocumentSearchStep, { isDisabled: total === 0, isUp: false, label: labels.nextMatch, onPress: onNext, testID: 'ps-document-search-next' }), isMaterial ? (_jsx(PsIconButton, { color: theme.white, iconKey: PsIconKey.Close, label: labels.closeSearch, onPress: onClose, size: 'compact', testID: 'ps-document-search-close' })) : (_jsx(Pressable, { accessibilityLabel: labels.closeSearch, accessibilityRole: 'button', hitSlop: PsLayoutConfig.hitSlop, onPress: onClose, testID: 'ps-document-search-close', children: _jsx(PsText, { tone: 'white', weight: 'semibold', children: labels.closeSearch }) }))] }));
|
|
46
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PsIconKey } from '@pallasoft/ps-icons';
|
|
3
|
+
import { Pressable, View } from 'react-native';
|
|
4
|
+
import { PsDocumentSearchConfig } from '../../constants/ps-document-search-config';
|
|
5
|
+
import { PsLayoutConfig } from '../../constants/ps-layout-config';
|
|
6
|
+
import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
|
|
7
|
+
import { PsRippleKind } from '../../constants/ps-ripple-kind';
|
|
8
|
+
import { usePsHaptics } from '../../hooks/use-ps-haptics';
|
|
9
|
+
import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
|
|
10
|
+
import { PsRippleService } from '../../services/ps-ripple-service';
|
|
11
|
+
import { PsStateService } from '../../services/ps-state-service';
|
|
12
|
+
import { PsIcon } from '../ps-icon/ps-icon';
|
|
13
|
+
export const PsDocumentSearchStep = ({ isDisabled, isUp, label, onPress, testID, }) => {
|
|
14
|
+
const haptic = usePsHaptics();
|
|
15
|
+
const theme = usePsMobileTheme();
|
|
16
|
+
return (_jsx(Pressable, { android_ripple: PsRippleService.resolve(theme, PsRippleKind.Borderless), accessibilityLabel: label, accessibilityRole: 'button', accessibilityState: { disabled: isDisabled }, disabled: isDisabled, hitSlop: PsLayoutConfig.hitSlop, onPress: () => {
|
|
17
|
+
haptic('light');
|
|
18
|
+
onPress();
|
|
19
|
+
}, style: {
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
height: PsMobileControlConfig.controlHeights.compact,
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
opacity: isDisabled ? PsStateService.disabledOpacity() : 1,
|
|
24
|
+
width: PsMobileControlConfig.controlHeights.compact,
|
|
25
|
+
}, testID: testID, children: _jsx(View, { style: {
|
|
26
|
+
transform: [
|
|
27
|
+
{ rotate: isUp ? PsDocumentSearchConfig.rotateUp : PsDocumentSearchConfig.rotateDown },
|
|
28
|
+
],
|
|
29
|
+
}, children: _jsx(PsIcon, { color: theme.white, iconKey: PsIconKey.Chevron }) }) }));
|
|
30
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PsDocumentItem } from '../../interfaces/ps-document-item';
|
|
2
2
|
import type { PsDocumentLabels } from '../../interfaces/ps-document-labels';
|
|
3
|
+
import type { PsDocumentSearchState } from '../../interfaces/ps-document-search-state';
|
|
3
4
|
export interface PsDocumentTextPageProps {
|
|
4
5
|
item: PsDocumentItem;
|
|
5
6
|
labels: PsDocumentLabels;
|
|
7
|
+
search?: PsDocumentSearchState;
|
|
6
8
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { PsDocumentTextPageProps } from './ps-document-text-page-props';
|
|
2
|
-
export declare const PsDocumentTextPage: ({ item, labels }: PsDocumentTextPageProps) => import("react").JSX.Element;
|
|
2
|
+
export declare const PsDocumentTextPage: ({ item, labels, search }: PsDocumentTextPageProps) => import("react").JSX.Element;
|
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
3
|
import { PsIconKey } from '@pallasoft/ps-icons';
|
|
3
|
-
import { ScrollView, View } from 'react-native';
|
|
4
|
+
import { ScrollView, Text, View } from 'react-native';
|
|
4
5
|
import { PsDocumentConfig } from '../../constants/ps-document-config';
|
|
5
6
|
import { PsDocumentLoadStatus } from '../../constants/ps-document-load-status';
|
|
7
|
+
import { PsDocumentSearchConfig } from '../../constants/ps-document-search-config';
|
|
6
8
|
import { usePsDocumentText } from '../../hooks/use-ps-document-text';
|
|
9
|
+
import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
|
|
10
|
+
import { PsDocumentSearchService } from '../../services/ps-document-search-service';
|
|
11
|
+
import { PsDocumentSearchStyleService } from '../../services/ps-document-search-style-service';
|
|
7
12
|
import { PsDocumentMessage } from '../ps-document-message/ps-document-message';
|
|
8
13
|
import { PsDocumentUnsupported } from '../ps-document-unsupported/ps-document-unsupported';
|
|
9
14
|
import { PsSpinner } from '../ps-spinner/ps-spinner';
|
|
10
15
|
import { PsText } from '../ps-text/ps-text';
|
|
11
|
-
export const PsDocumentTextPage = ({ item, labels }) => {
|
|
16
|
+
export const PsDocumentTextPage = ({ item, labels, search }) => {
|
|
12
17
|
const loaded = usePsDocumentText(item);
|
|
18
|
+
const theme = usePsMobileTheme();
|
|
19
|
+
const scroll = useRef(null);
|
|
20
|
+
const [lines, setLines] = useState([]);
|
|
21
|
+
const matches = loaded.text === null || search === undefined
|
|
22
|
+
? []
|
|
23
|
+
: PsDocumentSearchService.matches(loaded.text, search.query);
|
|
24
|
+
const active = search === undefined ? undefined : matches[search.activeIndex];
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (active === undefined)
|
|
27
|
+
return;
|
|
28
|
+
scroll.current?.scrollTo({
|
|
29
|
+
animated: true,
|
|
30
|
+
y: Math.max(0, PsDocumentSearchService.scrollOffset(lines, active.start) +
|
|
31
|
+
PsDocumentConfig.pageGutter -
|
|
32
|
+
PsDocumentSearchConfig.scrollMargin),
|
|
33
|
+
});
|
|
34
|
+
}, [active?.start, lines, search?.query]);
|
|
13
35
|
if (loaded.status === PsDocumentLoadStatus.Loading)
|
|
14
36
|
return (_jsx(View, { style: { alignItems: 'center', flex: 1, justifyContent: 'center' }, children: _jsx(PsSpinner, {}) }));
|
|
15
37
|
if (loaded.text === null)
|
|
16
38
|
return item.source === null ? (_jsx(PsDocumentUnsupported, { isOnDark: true, item: item, labels: labels })) : (_jsx(PsDocumentMessage, { description: item.name, iconKey: PsIconKey.Warning, isOnDark: true, title: labels.loadFailed }));
|
|
17
|
-
return (_jsx(View, { style: { flex: 1 }, testID: 'ps-document-text-page', children: _jsx(ScrollView, { contentContainerStyle: { padding: PsDocumentConfig.pageGutter }, showsVerticalScrollIndicator: true, children: _jsx(PsText, { isSelectable: true, tone: 'white', children:
|
|
39
|
+
return (_jsx(View, { style: { flex: 1 }, testID: 'ps-document-text-page', children: _jsx(ScrollView, { contentContainerStyle: { padding: PsDocumentConfig.pageGutter }, ref: scroll, showsVerticalScrollIndicator: true, children: _jsx(PsText, { isSelectable: true, onTextLayout: (event) => setLines(event.nativeEvent.lines.map((line) => ({ text: line.text, y: line.y }))), tone: 'white', children: matches.length === 0
|
|
40
|
+
? loaded.text
|
|
41
|
+
: PsDocumentSearchService.segments(loaded.text, matches).map((segment, index) => segment.matchIndex === null ? (segment.text) : (_jsx(Text, { style: PsDocumentSearchStyleService.span(theme, segment.matchIndex === search?.activeIndex), children: segment.text }, index))) }) }) }));
|
|
18
42
|
};
|
|
@@ -5,13 +5,17 @@ import { FlatList, Modal, StatusBar, View } from 'react-native';
|
|
|
5
5
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
6
6
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
7
7
|
import { PsDocumentConfig } from '../../constants/ps-document-config';
|
|
8
|
+
import { PsDocumentSearchConfig } from '../../constants/ps-document-search-config';
|
|
8
9
|
import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
|
|
9
10
|
import { usePsDocumentPdfPages } from '../../hooks/use-ps-document-pdf-pages';
|
|
11
|
+
import { usePsDocumentSearchTexts } from '../../hooks/use-ps-document-search-texts';
|
|
10
12
|
import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
|
|
11
13
|
import { PsDocumentPagerService } from '../../services/ps-document-pager-service';
|
|
14
|
+
import { PsDocumentSearchService } from '../../services/ps-document-search-service';
|
|
12
15
|
import { PsDocumentViewerEntryService } from '../../services/ps-document-viewer-entry-service';
|
|
13
16
|
import { PsDocumentPageStrip } from '../ps-document-page-strip/ps-document-page-strip';
|
|
14
17
|
import { PsDocumentPage } from '../ps-document-page/ps-document-page';
|
|
18
|
+
import { PsDocumentSearchBar } from '../ps-document-search-bar/ps-document-search-bar';
|
|
15
19
|
import { PsIconButton } from '../ps-icon-button/ps-icon-button';
|
|
16
20
|
import { PsText } from '../ps-text/ps-text';
|
|
17
21
|
export const PsDocumentViewer = ({ initialIndex = 0, isOpen, items, labels, onClose, }) => {
|
|
@@ -27,6 +31,43 @@ export const PsDocumentViewer = ({ initialIndex = 0, isOpen, items, labels, onCl
|
|
|
27
31
|
? PsDocumentViewerEntryService.startIndex(items, entries, initialIndex)
|
|
28
32
|
: PsDocumentViewerEntryService.resolveIndex(currentEntry, entries, 0);
|
|
29
33
|
const current = entries[index];
|
|
34
|
+
const searchLabels = PsDocumentSearchService.labels(labels);
|
|
35
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
36
|
+
const [query, setQuery] = useState('');
|
|
37
|
+
const [activeMatch, setActiveMatch] = useState(PsDocumentSearchConfig.noActive);
|
|
38
|
+
const searchTexts = usePsDocumentSearchTexts(items, isSearching);
|
|
39
|
+
const counts = PsDocumentSearchService.counts(entries, searchTexts, query);
|
|
40
|
+
const total = PsDocumentSearchService.total(counts);
|
|
41
|
+
const canSearch = searchLabels !== null && PsDocumentSearchService.isSearchable(items);
|
|
42
|
+
const showMatch = (match, nextCounts) => {
|
|
43
|
+
setActiveMatch(match);
|
|
44
|
+
const location = PsDocumentSearchService.locate(nextCounts, match);
|
|
45
|
+
if (location === null)
|
|
46
|
+
return;
|
|
47
|
+
const entry = entries[location.entryIndex];
|
|
48
|
+
if (entry !== undefined && location.entryIndex !== index)
|
|
49
|
+
setCurrentEntry(entry);
|
|
50
|
+
};
|
|
51
|
+
const closeSearch = () => {
|
|
52
|
+
setIsSearching(false);
|
|
53
|
+
setQuery('');
|
|
54
|
+
setActiveMatch(PsDocumentSearchConfig.noActive);
|
|
55
|
+
};
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!isSearching)
|
|
58
|
+
return;
|
|
59
|
+
if (total === 0) {
|
|
60
|
+
if (activeMatch !== PsDocumentSearchConfig.noActive)
|
|
61
|
+
setActiveMatch(PsDocumentSearchConfig.noActive);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (activeMatch < 0 || activeMatch >= total)
|
|
65
|
+
showMatch(PsDocumentSearchService.startIndex(counts, index), counts);
|
|
66
|
+
}, [isSearching, total, activeMatch]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!isOpen && isSearching)
|
|
69
|
+
closeSearch();
|
|
70
|
+
}, [isOpen]);
|
|
30
71
|
useEffect(() => {
|
|
31
72
|
if (width > 0 && entries.length > 0)
|
|
32
73
|
list.current?.scrollToIndex({ animated: false, index });
|
|
@@ -41,7 +82,10 @@ export const PsDocumentViewer = ({ initialIndex = 0, isOpen, items, labels, onCl
|
|
|
41
82
|
flexDirection: 'row',
|
|
42
83
|
paddingHorizontal: PsDocumentConfig.barPaddingHorizontal,
|
|
43
84
|
paddingVertical: PsDocumentConfig.barPaddingVertical,
|
|
44
|
-
}, children: [_jsx(PsIconButton, { color: theme.white, iconKey: PsIconKey.Close, label: labels.close, onPress: onClose }), _jsxs(View, { style: { alignItems: 'center', flex: 1 }, children: [PsDocumentPagerService.hasMany(entries.length) ? (_jsx(PsText, { tone: 'white', weight: 'semibold', children: PsDocumentPagerService.counter(index, entries.length) })) : null, current === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: 'white', variant: 'caption', children: current.item.name }))] }), _jsx(
|
|
85
|
+
}, children: [_jsx(PsIconButton, { color: theme.white, iconKey: PsIconKey.Close, label: labels.close, onPress: onClose }), _jsxs(View, { style: { alignItems: 'center', flex: 1 }, children: [PsDocumentPagerService.hasMany(entries.length) ? (_jsx(PsText, { tone: 'white', weight: 'semibold', children: PsDocumentPagerService.counter(index, entries.length) })) : null, current === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: 'white', variant: 'caption', children: current.item.name }))] }), canSearch && !isSearching ? (_jsx(PsIconButton, { color: theme.white, iconKey: PsIconKey.Search, label: searchLabels.search, onPress: () => setIsSearching(true), testID: 'ps-document-search-button' })) : (_jsx(View, { style: { width: PsMobileControlConfig.controlHeights.default } }))] }), canSearch && isSearching ? (_jsx(PsDocumentSearchBar, { activeIndex: activeMatch, labels: searchLabels, onChange: (value) => {
|
|
86
|
+
setQuery(value);
|
|
87
|
+
setActiveMatch(PsDocumentSearchConfig.noActive);
|
|
88
|
+
}, onClose: closeSearch, onNext: () => showMatch(PsDocumentSearchService.step(activeMatch, total, 1), counts), onPrevious: () => showMatch(PsDocumentSearchService.step(activeMatch, total, -1), counts), query: query, total: total })) : null, _jsx(View, { onLayout: (event) => setWidth(event.nativeEvent.layout.width), style: { flex: 1 }, testID: 'ps-document-viewer-pager', children: width === 0 ? null : (_jsx(FlatList, { data: entries, extraData: `${isSearching}|${query}|${activeMatch}|${total}`, getItemLayout: (_data, position) => ({
|
|
45
89
|
index: position,
|
|
46
90
|
length: width,
|
|
47
91
|
offset: PsDocumentPagerService.offset(position, width),
|
|
@@ -50,7 +94,12 @@ export const PsDocumentViewer = ({ initialIndex = 0, isOpen, items, labels, onCl
|
|
|
50
94
|
if (next !== undefined)
|
|
51
95
|
setCurrentEntry(next);
|
|
52
96
|
setIsZoomed(false);
|
|
53
|
-
}, pagingEnabled: true, ref: list, renderItem: ({ item: entry }) => (_jsx(View, { style: { width }, children: _jsx(PsDocumentPage, { item: entry.item, labels: labels, onDismiss: onClose, onRetry: pdf.retry, onZoomChange: setIsZoomed,
|
|
97
|
+
}, pagingEnabled: true, ref: list, renderItem: ({ index: position, item: entry }) => (_jsx(View, { style: { width }, children: _jsx(PsDocumentPage, { item: entry.item, labels: labels, onDismiss: onClose, onRetry: pdf.retry, onZoomChange: setIsZoomed, search: isSearching
|
|
98
|
+
? {
|
|
99
|
+
activeIndex: PsDocumentSearchService.activeFor(counts, activeMatch, position),
|
|
100
|
+
query,
|
|
101
|
+
}
|
|
102
|
+
: undefined, status: entry.status }) })), scrollEnabled: !isZoomed, showsHorizontalScrollIndicator: false, testID: 'ps-document-viewer-list' })) }), PsDocumentViewerEntryService.hasStrip(items, entries) ? (_jsx(PsDocumentPageStrip, { entries: entries, index: index, onSelect: (position) => {
|
|
54
103
|
const entry = entries[position];
|
|
55
104
|
if (entry !== undefined)
|
|
56
105
|
setCurrentEntry(entry);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { PsDocumentSearchState } from '../../interfaces/ps-document-search-state';
|
|
1
2
|
import type { PsDocumentSource } from '../../interfaces/ps-document-source';
|
|
3
|
+
import type { PsDocumentTextLayer } from '../../interfaces/ps-document-text-layer';
|
|
2
4
|
export interface PsDocumentZoomImageProps {
|
|
3
5
|
name: string;
|
|
4
6
|
onDismiss: () => void;
|
|
5
7
|
onError: () => void;
|
|
6
8
|
onZoomChange: (isZoomed: boolean) => void;
|
|
9
|
+
search?: PsDocumentSearchState;
|
|
7
10
|
source: PsDocumentSource;
|
|
11
|
+
textLayer?: PsDocumentTextLayer;
|
|
8
12
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { PsDocumentZoomImageProps } from './ps-document-zoom-image-props';
|
|
2
|
-
export declare const PsDocumentZoomImage: ({ name, onDismiss, onError, onZoomChange, source, }: PsDocumentZoomImageProps) => import("react").JSX.Element;
|
|
2
|
+
export declare const PsDocumentZoomImage: ({ name, onDismiss, onError, onZoomChange, search, source, textLayer, }: PsDocumentZoomImageProps) => import("react").JSX.Element;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
3
|
import { Image, View } from 'react-native';
|
|
4
4
|
import { GestureDetector, usePanGesture, usePinchGesture, useSimultaneousGestures, useTapGesture, } from 'react-native-gesture-handler';
|
|
5
5
|
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
6
6
|
import { PsDocumentConfig } from '../../constants/ps-document-config';
|
|
7
|
+
import { PsDocumentSearchService } from '../../services/ps-document-search-service';
|
|
7
8
|
import { PsDocumentZoomService } from '../../services/ps-document-zoom-service';
|
|
9
|
+
import { PsDocumentHighlightLayer } from '../ps-document-highlight-layer/ps-document-highlight-layer';
|
|
8
10
|
import { PsSpinner } from '../ps-spinner/ps-spinner';
|
|
9
|
-
export const PsDocumentZoomImage = ({ name, onDismiss, onError, onZoomChange, source, }) => {
|
|
11
|
+
export const PsDocumentZoomImage = ({ name, onDismiss, onError, onZoomChange, search, source, textLayer, }) => {
|
|
10
12
|
const [size, setSize] = useState({ height: 0, width: 0 });
|
|
11
13
|
const [isZoomed, setIsZoomed] = useState(false);
|
|
12
14
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
@@ -83,6 +85,21 @@ export const PsDocumentZoomImage = ({ name, onDismiss, onError, onZoomChange, so
|
|
|
83
85
|
},
|
|
84
86
|
runOnJS: true,
|
|
85
87
|
});
|
|
88
|
+
const hasSearch = search !== undefined && textLayer !== undefined && search.activeIndex >= 0;
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!hasSearch || !isZoomed || size.width === 0)
|
|
91
|
+
return;
|
|
92
|
+
const active = PsDocumentSearchService.highlights(textLayer, search.query, size, search.activeIndex).find((rect) => rect.isActive);
|
|
93
|
+
if (active === undefined)
|
|
94
|
+
return;
|
|
95
|
+
const center = PsDocumentSearchService.center(active.box);
|
|
96
|
+
const nextX = PsDocumentZoomService.centerOffset(center.x, size.width, scale.value);
|
|
97
|
+
const nextY = PsDocumentZoomService.centerOffset(center.y, size.height, scale.value);
|
|
98
|
+
x.value = withTiming(nextX, { duration: PsDocumentConfig.settleMs });
|
|
99
|
+
y.value = withTiming(nextY, { duration: PsDocumentConfig.settleMs });
|
|
100
|
+
baseX.value = nextX;
|
|
101
|
+
baseY.value = nextY;
|
|
102
|
+
}, [search?.activeIndex, search?.query]);
|
|
86
103
|
const gesture = useSimultaneousGestures(pinch, move, dismiss, doubleTap);
|
|
87
104
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
88
105
|
transform: [
|
|
@@ -92,7 +109,7 @@ export const PsDocumentZoomImage = ({ name, onDismiss, onError, onZoomChange, so
|
|
|
92
109
|
{ scale: scale.value },
|
|
93
110
|
],
|
|
94
111
|
}));
|
|
95
|
-
return (_jsx(GestureDetector, { gesture: gesture, children: _jsxs(View, { onLayout: (event) => setSize(event.nativeEvent.layout), style: { flex: 1, overflow: 'hidden' }, testID: 'ps-document-zoom-image', children: [
|
|
112
|
+
return (_jsx(GestureDetector, { gesture: gesture, children: _jsxs(View, { onLayout: (event) => setSize(event.nativeEvent.layout), style: { flex: 1, overflow: 'hidden' }, testID: 'ps-document-zoom-image', children: [_jsxs(Animated.View, { style: [{ flex: 1 }, animatedStyle], children: [_jsx(Image, { accessibilityLabel: name, onError: onError, onLoad: () => setIsLoaded(true), resizeMode: 'contain', source: { headers: source.headers, uri: source.uri }, style: { flex: 1 } }), search === undefined || textLayer === undefined ? null : (_jsx(PsDocumentHighlightLayer, { layer: textLayer, search: search, size: size }))] }), isLoaded ? null : (_jsx(View, { pointerEvents: 'none', style: {
|
|
96
113
|
alignItems: 'center',
|
|
97
114
|
bottom: 0,
|
|
98
115
|
justifyContent: 'center',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { PsTextProps } from './ps-text-props';
|
|
2
|
-
export declare const PsText: ({ accessibilityRole, align, children, ellipsizeMode, isSelectable, numberOfLines, onPress, style, testID, tone, variant, weight, }: PsTextProps) => import("react").JSX.Element;
|
|
2
|
+
export declare const PsText: ({ accessibilityRole, align, children, ellipsizeMode, isSelectable, numberOfLines, onPress, onTextLayout, style, testID, tone, variant, weight, }: PsTextProps) => import("react").JSX.Element;
|
|
@@ -2,9 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { StyleSheet, Text } from 'react-native';
|
|
3
3
|
import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
|
|
4
4
|
import { PsTextService } from '../../services/ps-text-service';
|
|
5
|
-
export const PsText = ({ accessibilityRole, align, children, ellipsizeMode = 'tail', isSelectable = false, numberOfLines, onPress, style, testID, tone = 'ink', variant = 'body', weight, }) => {
|
|
5
|
+
export const PsText = ({ accessibilityRole, align, children, ellipsizeMode = 'tail', isSelectable = false, numberOfLines, onPress, onTextLayout, style, testID, tone = 'ink', variant = 'body', weight, }) => {
|
|
6
6
|
const theme = usePsMobileTheme();
|
|
7
|
-
return (_jsx(Text, { accessibilityRole: accessibilityRole, ellipsizeMode: ellipsizeMode, numberOfLines: numberOfLines, onPress: onPress, selectable: isSelectable, style: StyleSheet.flatten([
|
|
7
|
+
return (_jsx(Text, { accessibilityRole: accessibilityRole, ellipsizeMode: ellipsizeMode, numberOfLines: numberOfLines, onPress: onPress, onTextLayout: onTextLayout, selectable: isSelectable, style: StyleSheet.flatten([
|
|
8
8
|
PsTextService.style(variant, tone, weight, theme),
|
|
9
9
|
{ textAlign: align },
|
|
10
10
|
style,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const PsDocumentSearchConfig: {
|
|
2
|
+
readonly activeAlpha: 0.9;
|
|
3
|
+
readonly androidFieldRadius: 999;
|
|
4
|
+
readonly barGap: 8;
|
|
5
|
+
readonly combiningMarkPattern: RegExp;
|
|
6
|
+
readonly currentToken: "{current}";
|
|
7
|
+
readonly fieldAlpha: 0.14;
|
|
8
|
+
readonly fieldPaddingX: 12;
|
|
9
|
+
readonly highlightAlpha: 0.4;
|
|
10
|
+
readonly highlightRadius: 3;
|
|
11
|
+
readonly iosFieldRadius: 10;
|
|
12
|
+
readonly noActive: -1;
|
|
13
|
+
readonly rotateDown: "0deg";
|
|
14
|
+
readonly rotateUp: "180deg";
|
|
15
|
+
readonly scrollMargin: 48;
|
|
16
|
+
readonly termSeparatorPattern: RegExp;
|
|
17
|
+
readonly totalToken: "{total}";
|
|
18
|
+
readonly wordSeparator: " ";
|
|
19
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const PsDocumentSearchConfig = {
|
|
2
|
+
activeAlpha: 0.9,
|
|
3
|
+
androidFieldRadius: 999,
|
|
4
|
+
barGap: 8,
|
|
5
|
+
combiningMarkPattern: /\p{M}/gu,
|
|
6
|
+
currentToken: '{current}',
|
|
7
|
+
fieldAlpha: 0.14,
|
|
8
|
+
fieldPaddingX: 12,
|
|
9
|
+
highlightAlpha: 0.4,
|
|
10
|
+
highlightRadius: 3,
|
|
11
|
+
iosFieldRadius: 10,
|
|
12
|
+
noActive: -1,
|
|
13
|
+
rotateDown: '0deg',
|
|
14
|
+
rotateUp: '180deg',
|
|
15
|
+
scrollMargin: 48,
|
|
16
|
+
termSeparatorPattern: /\s+/u,
|
|
17
|
+
totalToken: '{total}',
|
|
18
|
+
wordSeparator: ' ',
|
|
19
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { PsDocumentTextService } from '../services/ps-document-text-service';
|
|
3
|
+
export const usePsDocumentSearchTexts = (items, isActive) => {
|
|
4
|
+
const [texts, setTexts] = useState({});
|
|
5
|
+
const key = items
|
|
6
|
+
.filter((item) => PsDocumentTextService.needsLoad(item))
|
|
7
|
+
.map((item) => item.id)
|
|
8
|
+
.join('|');
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!isActive || key === '')
|
|
11
|
+
return undefined;
|
|
12
|
+
let isCurrent = true;
|
|
13
|
+
items
|
|
14
|
+
.filter((item) => PsDocumentTextService.needsLoad(item))
|
|
15
|
+
.forEach((item) => {
|
|
16
|
+
void PsDocumentTextService.load(item).then((text) => {
|
|
17
|
+
if (isCurrent && text !== null)
|
|
18
|
+
setTexts((previous) => ({ ...previous, [item.id]: text }));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
return () => {
|
|
22
|
+
isCurrent = false;
|
|
23
|
+
};
|
|
24
|
+
}, [isActive, key]);
|
|
25
|
+
return texts;
|
|
26
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -77,7 +77,11 @@ export * from './interfaces/ps-device-file';
|
|
|
77
77
|
export * from './interfaces/ps-document-item';
|
|
78
78
|
export * from './interfaces/ps-document-labels';
|
|
79
79
|
export * from './interfaces/ps-document-scanner-options';
|
|
80
|
+
export * from './interfaces/ps-document-search-labels';
|
|
80
81
|
export * from './interfaces/ps-document-source';
|
|
82
|
+
export * from './interfaces/ps-document-text-box';
|
|
83
|
+
export * from './interfaces/ps-document-text-layer';
|
|
84
|
+
export * from './interfaces/ps-document-text-layer-word';
|
|
81
85
|
export * from './interfaces/ps-file-upload-options';
|
|
82
86
|
export * from './interfaces/use-ps-file-upload-result';
|
|
83
87
|
export * from './interfaces/ps-tab-bar-context-data';
|
|
@@ -107,6 +111,7 @@ export { PsActionMenuService } from './services/ps-action-menu-service';
|
|
|
107
111
|
export { PsSelectionService } from './services/ps-selection-service';
|
|
108
112
|
export { PsSortableGridService } from './services/ps-sortable-grid-service';
|
|
109
113
|
export { PsDocumentPagerService } from './services/ps-document-pager-service';
|
|
114
|
+
export { PsDocumentSearchService } from './services/ps-document-search-service';
|
|
110
115
|
export { PsDocumentZoomService } from './services/ps-document-zoom-service';
|
|
111
116
|
export { PsDocumentScannerService } from './services/ps-document-scanner-service';
|
|
112
117
|
export { PsCapturePickerService } from './services/ps-capture-picker-service';
|
package/dist/index.js
CHANGED
|
@@ -76,7 +76,11 @@ export * from './interfaces/ps-device-file';
|
|
|
76
76
|
export * from './interfaces/ps-document-item';
|
|
77
77
|
export * from './interfaces/ps-document-labels';
|
|
78
78
|
export * from './interfaces/ps-document-scanner-options';
|
|
79
|
+
export * from './interfaces/ps-document-search-labels';
|
|
79
80
|
export * from './interfaces/ps-document-source';
|
|
81
|
+
export * from './interfaces/ps-document-text-box';
|
|
82
|
+
export * from './interfaces/ps-document-text-layer';
|
|
83
|
+
export * from './interfaces/ps-document-text-layer-word';
|
|
80
84
|
export * from './interfaces/ps-file-upload-options';
|
|
81
85
|
export * from './interfaces/use-ps-file-upload-result';
|
|
82
86
|
export * from './interfaces/ps-tab-bar-context-data';
|
|
@@ -106,6 +110,7 @@ export { PsActionMenuService } from './services/ps-action-menu-service';
|
|
|
106
110
|
export { PsSelectionService } from './services/ps-selection-service';
|
|
107
111
|
export { PsSortableGridService } from './services/ps-sortable-grid-service';
|
|
108
112
|
export { PsDocumentPagerService } from './services/ps-document-pager-service';
|
|
113
|
+
export { PsDocumentSearchService } from './services/ps-document-search-service';
|
|
109
114
|
export { PsDocumentZoomService } from './services/ps-document-zoom-service';
|
|
110
115
|
export { PsDocumentScannerService } from './services/ps-document-scanner-service';
|
|
111
116
|
export { PsCapturePickerService } from './services/ps-capture-picker-service';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { PsDocumentKind } from '../constants/ps-document-kind';
|
|
2
2
|
import type { PsDocumentSource } from './ps-document-source';
|
|
3
|
+
import type { PsDocumentTextLayer } from './ps-document-text-layer';
|
|
3
4
|
export interface PsDocumentItem {
|
|
4
5
|
id: string;
|
|
5
6
|
kind: PsDocumentKind;
|
|
6
7
|
name: string;
|
|
8
|
+
pageTextLayers?: PsDocumentTextLayer[];
|
|
7
9
|
source: PsDocumentSource | null;
|
|
8
10
|
text: string | null;
|
|
11
|
+
textLayer?: PsDocumentTextLayer;
|
|
9
12
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
export interface PsDocumentLabels {
|
|
2
2
|
close: string;
|
|
3
|
+
closeSearch?: string;
|
|
3
4
|
empty: string;
|
|
4
5
|
error: string;
|
|
5
6
|
expand: string;
|
|
6
7
|
loadFailed: string;
|
|
8
|
+
matchCount?: string;
|
|
9
|
+
nextMatch?: string;
|
|
10
|
+
noMatches?: string;
|
|
7
11
|
open?: string;
|
|
12
|
+
previousMatch?: string;
|
|
8
13
|
retry: string;
|
|
14
|
+
search?: string;
|
|
15
|
+
searchPlaceholder?: string;
|
|
9
16
|
unsupported: string;
|
|
10
17
|
}
|
|
@@ -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,40 @@
|
|
|
1
|
+
import type { PsDocumentHighlightRect } from '../interfaces/ps-document-highlight-rect';
|
|
2
|
+
import type { PsDocumentItem } from '../interfaces/ps-document-item';
|
|
3
|
+
import type { PsDocumentLabels } from '../interfaces/ps-document-labels';
|
|
4
|
+
import type { PsDocumentNormalizedText } from '../interfaces/ps-document-normalized-text';
|
|
5
|
+
import type { PsDocumentPoint } from '../interfaces/ps-document-point';
|
|
6
|
+
import type { PsDocumentSearchLabels } from '../interfaces/ps-document-search-labels';
|
|
7
|
+
import type { PsDocumentSearchLocation } from '../interfaces/ps-document-search-location';
|
|
8
|
+
import type { PsDocumentSearchMatch } from '../interfaces/ps-document-search-match';
|
|
9
|
+
import type { PsDocumentTextBox } from '../interfaces/ps-document-text-box';
|
|
10
|
+
import type { PsDocumentTextLayer } from '../interfaces/ps-document-text-layer';
|
|
11
|
+
import type { PsDocumentTextLine } from '../interfaces/ps-document-text-line';
|
|
12
|
+
import type { PsDocumentTextSegment } from '../interfaces/ps-document-text-segment';
|
|
13
|
+
import type { PsDocumentViewerEntry } from '../interfaces/ps-document-viewer-entry';
|
|
14
|
+
import type { PsDocumentWordMatch } from '../interfaces/ps-document-word-match';
|
|
15
|
+
import type { PsSize } from '../interfaces/ps-size';
|
|
16
|
+
export declare class PsDocumentSearchService {
|
|
17
|
+
static normalize(text: string): PsDocumentNormalizedText;
|
|
18
|
+
static terms(query: string): string[];
|
|
19
|
+
static matches(text: string, query: string): PsDocumentSearchMatch[];
|
|
20
|
+
static segments(text: string, matches: PsDocumentSearchMatch[]): PsDocumentTextSegment[];
|
|
21
|
+
static joinedWords(layer: PsDocumentTextLayer): string;
|
|
22
|
+
static wordMatches(layer: PsDocumentTextLayer, query: string): PsDocumentWordMatch[];
|
|
23
|
+
static fit(layer: PsDocumentTextLayer, container: PsSize): PsDocumentTextBox;
|
|
24
|
+
static rect(box: PsDocumentTextBox, layer: PsDocumentTextLayer, container: PsSize): PsDocumentTextBox;
|
|
25
|
+
static highlights(layer: PsDocumentTextLayer, query: string, container: PsSize, activeIndex: number): PsDocumentHighlightRect[];
|
|
26
|
+
static center(box: PsDocumentTextBox): PsDocumentPoint;
|
|
27
|
+
static scrollOffset(lines: PsDocumentTextLine[], start: number): number;
|
|
28
|
+
static entryMatchCount(entry: PsDocumentViewerEntry, texts: Record<string, string>, query: string): number;
|
|
29
|
+
static counts(entries: PsDocumentViewerEntry[], texts: Record<string, string>, query: string): number[];
|
|
30
|
+
static total(counts: number[]): number;
|
|
31
|
+
static locate(counts: number[], active: number): PsDocumentSearchLocation | null;
|
|
32
|
+
static activeFor(counts: number[], active: number, entryIndex: number): number;
|
|
33
|
+
static firstIndexOf(counts: number[], entryIndex: number): number;
|
|
34
|
+
static step(active: number, total: number, direction: 1 | -1): number;
|
|
35
|
+
static startIndex(counts: number[], entryIndex: number): number;
|
|
36
|
+
static countLabel(template: string, current: number, total: number): string;
|
|
37
|
+
static labels(labels: PsDocumentLabels): PsDocumentSearchLabels | null;
|
|
38
|
+
static isSearchable(items: PsDocumentItem[]): boolean;
|
|
39
|
+
private static merge;
|
|
40
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { PsDocumentKind } from '../constants/ps-document-kind';
|
|
2
|
+
import { PsDocumentSearchConfig } from '../constants/ps-document-search-config';
|
|
3
|
+
export class PsDocumentSearchService {
|
|
4
|
+
static normalize(text) {
|
|
5
|
+
const starts = [];
|
|
6
|
+
const ends = [];
|
|
7
|
+
let value = '';
|
|
8
|
+
let offset = 0;
|
|
9
|
+
for (const character of Array.from(text)) {
|
|
10
|
+
const folded = character
|
|
11
|
+
.normalize('NFD')
|
|
12
|
+
.replace(PsDocumentSearchConfig.combiningMarkPattern, '')
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
for (let position = 0; position < folded.length; position += 1) {
|
|
15
|
+
starts.push(offset);
|
|
16
|
+
ends.push(offset + character.length);
|
|
17
|
+
}
|
|
18
|
+
value += folded;
|
|
19
|
+
offset += character.length;
|
|
20
|
+
}
|
|
21
|
+
return { ends, starts, value };
|
|
22
|
+
}
|
|
23
|
+
static terms(query) {
|
|
24
|
+
return PsDocumentSearchService.normalize(query)
|
|
25
|
+
.value.split(PsDocumentSearchConfig.termSeparatorPattern)
|
|
26
|
+
.filter((term) => term !== '');
|
|
27
|
+
}
|
|
28
|
+
static matches(text, query) {
|
|
29
|
+
const terms = PsDocumentSearchService.terms(query);
|
|
30
|
+
if (terms.length === 0)
|
|
31
|
+
return [];
|
|
32
|
+
const normalized = PsDocumentSearchService.normalize(text);
|
|
33
|
+
const ranges = [];
|
|
34
|
+
for (const term of terms) {
|
|
35
|
+
let from = normalized.value.indexOf(term);
|
|
36
|
+
while (from >= 0) {
|
|
37
|
+
ranges.push({ end: from + term.length, start: from });
|
|
38
|
+
from = normalized.value.indexOf(term, from + term.length);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return PsDocumentSearchService.merge(ranges).map((range) => ({
|
|
42
|
+
end: normalized.ends[range.end - 1],
|
|
43
|
+
start: normalized.starts[range.start],
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
static segments(text, matches) {
|
|
47
|
+
const segments = [];
|
|
48
|
+
let cursor = 0;
|
|
49
|
+
matches.forEach((match, index) => {
|
|
50
|
+
if (match.start > cursor)
|
|
51
|
+
segments.push({ matchIndex: null, text: text.slice(cursor, match.start) });
|
|
52
|
+
segments.push({ matchIndex: index, text: text.slice(match.start, match.end) });
|
|
53
|
+
cursor = match.end;
|
|
54
|
+
});
|
|
55
|
+
if (cursor < text.length)
|
|
56
|
+
segments.push({ matchIndex: null, text: text.slice(cursor) });
|
|
57
|
+
return segments;
|
|
58
|
+
}
|
|
59
|
+
static joinedWords(layer) {
|
|
60
|
+
return layer.words.map((word) => word.text).join(PsDocumentSearchConfig.wordSeparator);
|
|
61
|
+
}
|
|
62
|
+
static wordMatches(layer, query) {
|
|
63
|
+
const matches = PsDocumentSearchService.matches(PsDocumentSearchService.joinedWords(layer), query);
|
|
64
|
+
const bounds = [];
|
|
65
|
+
let cursor = 0;
|
|
66
|
+
for (const word of layer.words) {
|
|
67
|
+
bounds.push({ end: cursor + word.text.length, start: cursor });
|
|
68
|
+
cursor += word.text.length + PsDocumentSearchConfig.wordSeparator.length;
|
|
69
|
+
}
|
|
70
|
+
return matches.map((match) => ({
|
|
71
|
+
wordIndices: bounds.flatMap((bound, index) => bound.start < match.end && bound.end > match.start ? [index] : []),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
static fit(layer, container) {
|
|
75
|
+
if (layer.width <= 0 || layer.height <= 0)
|
|
76
|
+
return { height: 0, left: 0, top: 0, width: 0 };
|
|
77
|
+
const scale = Math.min(container.width / layer.width, container.height / layer.height);
|
|
78
|
+
const width = layer.width * scale;
|
|
79
|
+
const height = layer.height * scale;
|
|
80
|
+
return {
|
|
81
|
+
height,
|
|
82
|
+
left: (container.width - width) / 2,
|
|
83
|
+
top: (container.height - height) / 2,
|
|
84
|
+
width,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
static rect(box, layer, container) {
|
|
88
|
+
const fitted = PsDocumentSearchService.fit(layer, container);
|
|
89
|
+
const scale = layer.width <= 0 ? 0 : fitted.width / layer.width;
|
|
90
|
+
return {
|
|
91
|
+
height: box.height * scale,
|
|
92
|
+
left: fitted.left + box.left * scale,
|
|
93
|
+
top: fitted.top + box.top * scale,
|
|
94
|
+
width: box.width * scale,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
static highlights(layer, query, container, activeIndex) {
|
|
98
|
+
return PsDocumentSearchService.wordMatches(layer, query).flatMap((match, matchIndex) => match.wordIndices.map((wordIndex) => ({
|
|
99
|
+
box: PsDocumentSearchService.rect(layer.words[wordIndex].box, layer, container),
|
|
100
|
+
isActive: matchIndex === activeIndex,
|
|
101
|
+
})));
|
|
102
|
+
}
|
|
103
|
+
static center(box) {
|
|
104
|
+
return { x: box.left + box.width / 2, y: box.top + box.height / 2 };
|
|
105
|
+
}
|
|
106
|
+
static scrollOffset(lines, start) {
|
|
107
|
+
let cursor = 0;
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
cursor += line.text.length;
|
|
110
|
+
if (start < cursor)
|
|
111
|
+
return line.y;
|
|
112
|
+
}
|
|
113
|
+
const last = lines[lines.length - 1];
|
|
114
|
+
return last === undefined ? 0 : last.y;
|
|
115
|
+
}
|
|
116
|
+
static entryMatchCount(entry, texts, query) {
|
|
117
|
+
if (entry.item.kind === PsDocumentKind.Text) {
|
|
118
|
+
const text = entry.item.text === null ? texts[entry.item.id] : entry.item.text;
|
|
119
|
+
return text === undefined ? 0 : PsDocumentSearchService.matches(text, query).length;
|
|
120
|
+
}
|
|
121
|
+
const layer = entry.item.textLayer;
|
|
122
|
+
return layer === undefined ? 0 : PsDocumentSearchService.wordMatches(layer, query).length;
|
|
123
|
+
}
|
|
124
|
+
static counts(entries, texts, query) {
|
|
125
|
+
return entries.map((entry) => PsDocumentSearchService.entryMatchCount(entry, texts, query));
|
|
126
|
+
}
|
|
127
|
+
static total(counts) {
|
|
128
|
+
return counts.reduce((sum, count) => sum + count, 0);
|
|
129
|
+
}
|
|
130
|
+
static locate(counts, active) {
|
|
131
|
+
let remaining = active;
|
|
132
|
+
for (let entryIndex = 0; entryIndex < counts.length; entryIndex += 1) {
|
|
133
|
+
if (remaining < counts[entryIndex])
|
|
134
|
+
return { entryIndex, localIndex: remaining };
|
|
135
|
+
remaining -= counts[entryIndex];
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
static activeFor(counts, active, entryIndex) {
|
|
140
|
+
const location = PsDocumentSearchService.locate(counts, active);
|
|
141
|
+
return location !== null && location.entryIndex === entryIndex
|
|
142
|
+
? location.localIndex
|
|
143
|
+
: PsDocumentSearchConfig.noActive;
|
|
144
|
+
}
|
|
145
|
+
static firstIndexOf(counts, entryIndex) {
|
|
146
|
+
return counts.slice(0, entryIndex).reduce((sum, count) => sum + count, 0);
|
|
147
|
+
}
|
|
148
|
+
static step(active, total, direction) {
|
|
149
|
+
if (total <= 0)
|
|
150
|
+
return PsDocumentSearchConfig.noActive;
|
|
151
|
+
return (active + direction + total) % total;
|
|
152
|
+
}
|
|
153
|
+
static startIndex(counts, entryIndex) {
|
|
154
|
+
const total = PsDocumentSearchService.total(counts);
|
|
155
|
+
if (total === 0)
|
|
156
|
+
return PsDocumentSearchConfig.noActive;
|
|
157
|
+
return counts[entryIndex] > 0 ? PsDocumentSearchService.firstIndexOf(counts, entryIndex) : 0;
|
|
158
|
+
}
|
|
159
|
+
static countLabel(template, current, total) {
|
|
160
|
+
return template
|
|
161
|
+
.replace(PsDocumentSearchConfig.currentToken, String(current))
|
|
162
|
+
.replace(PsDocumentSearchConfig.totalToken, String(total));
|
|
163
|
+
}
|
|
164
|
+
static labels(labels) {
|
|
165
|
+
const { closeSearch, matchCount, nextMatch, noMatches, previousMatch, search, searchPlaceholder, } = labels;
|
|
166
|
+
if (closeSearch === undefined ||
|
|
167
|
+
matchCount === undefined ||
|
|
168
|
+
nextMatch === undefined ||
|
|
169
|
+
noMatches === undefined ||
|
|
170
|
+
previousMatch === undefined ||
|
|
171
|
+
search === undefined ||
|
|
172
|
+
searchPlaceholder === undefined)
|
|
173
|
+
return null;
|
|
174
|
+
return {
|
|
175
|
+
closeSearch,
|
|
176
|
+
matchCount,
|
|
177
|
+
nextMatch,
|
|
178
|
+
noMatches,
|
|
179
|
+
previousMatch,
|
|
180
|
+
search,
|
|
181
|
+
searchPlaceholder,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
static isSearchable(items) {
|
|
185
|
+
return items.some((item) => (item.kind === PsDocumentKind.Text && (item.text !== null || item.source !== null)) ||
|
|
186
|
+
item.textLayer !== undefined ||
|
|
187
|
+
(item.pageTextLayers !== undefined && item.pageTextLayers.length > 0));
|
|
188
|
+
}
|
|
189
|
+
static merge(ranges) {
|
|
190
|
+
const sorted = [...ranges].sort((a, b) => a.start === b.start ? a.end - b.end : a.start - b.start);
|
|
191
|
+
const merged = [];
|
|
192
|
+
for (const range of sorted) {
|
|
193
|
+
const last = merged[merged.length - 1];
|
|
194
|
+
if (last !== undefined && range.start < last.end)
|
|
195
|
+
merged[merged.length - 1] = { end: Math.max(last.end, range.end), start: last.start };
|
|
196
|
+
else
|
|
197
|
+
merged.push({ ...range });
|
|
198
|
+
}
|
|
199
|
+
return merged;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TextStyle, ViewStyle } from 'react-native';
|
|
2
|
+
import type { PsMobileTheme } from '../interfaces/ps-mobile-theme';
|
|
3
|
+
export declare class PsDocumentSearchStyleService {
|
|
4
|
+
static span(theme: PsMobileTheme, isActive: boolean): TextStyle;
|
|
5
|
+
static rect(theme: PsMobileTheme, isActive: boolean): ViewStyle;
|
|
6
|
+
static fieldRadius(isMaterial: boolean): number;
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PsDocumentSearchConfig } from '../constants/ps-document-search-config';
|
|
2
|
+
import { PsMobileColorService } from './ps-mobile-color-service';
|
|
3
|
+
export class PsDocumentSearchStyleService {
|
|
4
|
+
static span(theme, isActive) {
|
|
5
|
+
return isActive
|
|
6
|
+
? { backgroundColor: theme.primary, color: theme.primaryContrast }
|
|
7
|
+
: {
|
|
8
|
+
backgroundColor: PsMobileColorService.withAlpha(theme.primary, PsDocumentSearchConfig.highlightAlpha),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
static rect(theme, isActive) {
|
|
12
|
+
return {
|
|
13
|
+
backgroundColor: PsMobileColorService.withAlpha(theme.primary, isActive ? PsDocumentSearchConfig.activeAlpha : PsDocumentSearchConfig.highlightAlpha),
|
|
14
|
+
borderRadius: PsDocumentSearchConfig.highlightRadius,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
static fieldRadius(isMaterial) {
|
|
18
|
+
return isMaterial
|
|
19
|
+
? PsDocumentSearchConfig.androidFieldRadius
|
|
20
|
+
: PsDocumentSearchConfig.iosFieldRadius;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -63,6 +63,7 @@ export class PsDocumentViewerEntryService {
|
|
|
63
63
|
name: item.name,
|
|
64
64
|
source: { headers: {}, uri: page.uri },
|
|
65
65
|
text: null,
|
|
66
|
+
textLayer: item.pageTextLayers === undefined ? undefined : item.pageTextLayers[index],
|
|
66
67
|
},
|
|
67
68
|
sourceId: item.id,
|
|
68
69
|
status: PsDocumentLoadStatus.Ready,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare class PsDocumentZoomService {
|
|
2
2
|
static clampAxis(value: number, container: number, scale: number): number;
|
|
3
|
+
static centerOffset(point: number, container: number, scale: number): number;
|
|
3
4
|
static clampScale(scale: number): number;
|
|
4
5
|
static dragOffset(translationY: number): number;
|
|
5
6
|
static focalOffset(tap: number, container: number, scale: number): number;
|
|
@@ -4,6 +4,9 @@ export class PsDocumentZoomService {
|
|
|
4
4
|
const limit = (container * (scale - 1)) / 2;
|
|
5
5
|
return Math.min(limit, Math.max(-limit, value));
|
|
6
6
|
}
|
|
7
|
+
static centerOffset(point, container, scale) {
|
|
8
|
+
return PsDocumentZoomService.clampAxis((container / 2 - point) * scale, container, scale);
|
|
9
|
+
}
|
|
7
10
|
static clampScale(scale) {
|
|
8
11
|
return Math.min(PsDocumentConfig.maxZoom, Math.max(PsDocumentConfig.minZoom, scale));
|
|
9
12
|
}
|
package/package.json
CHANGED