@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.
Files changed (73) hide show
  1. package/dist/components/ps-document-highlight-layer/index.d.ts +2 -0
  2. package/dist/components/ps-document-highlight-layer/index.js +2 -0
  3. package/dist/components/ps-document-highlight-layer/ps-document-highlight-layer-props.d.ts +8 -0
  4. package/dist/components/ps-document-highlight-layer/ps-document-highlight-layer-props.js +1 -0
  5. package/dist/components/ps-document-highlight-layer/ps-document-highlight-layer.d.ts +2 -0
  6. package/dist/components/ps-document-highlight-layer/ps-document-highlight-layer.js +19 -0
  7. package/dist/components/ps-document-page/ps-document-page-props.d.ts +2 -0
  8. package/dist/components/ps-document-page/ps-document-page.d.ts +1 -1
  9. package/dist/components/ps-document-page/ps-document-page.js +3 -3
  10. package/dist/components/ps-document-search-bar/index.d.ts +2 -0
  11. package/dist/components/ps-document-search-bar/index.js +2 -0
  12. package/dist/components/ps-document-search-bar/ps-document-search-bar-props.d.ts +11 -0
  13. package/dist/components/ps-document-search-bar/ps-document-search-bar-props.js +1 -0
  14. package/dist/components/ps-document-search-bar/ps-document-search-bar.d.ts +2 -0
  15. package/dist/components/ps-document-search-bar/ps-document-search-bar.js +46 -0
  16. package/dist/components/ps-document-search-step/index.d.ts +2 -0
  17. package/dist/components/ps-document-search-step/index.js +2 -0
  18. package/dist/components/ps-document-search-step/ps-document-search-step-props.d.ts +7 -0
  19. package/dist/components/ps-document-search-step/ps-document-search-step-props.js +1 -0
  20. package/dist/components/ps-document-search-step/ps-document-search-step.d.ts +2 -0
  21. package/dist/components/ps-document-search-step/ps-document-search-step.js +30 -0
  22. package/dist/components/ps-document-text-page/ps-document-text-page-props.d.ts +2 -0
  23. package/dist/components/ps-document-text-page/ps-document-text-page.d.ts +1 -1
  24. package/dist/components/ps-document-text-page/ps-document-text-page.js +27 -3
  25. package/dist/components/ps-document-viewer/ps-document-viewer.js +51 -2
  26. package/dist/components/ps-document-zoom-image/ps-document-zoom-image-props.d.ts +4 -0
  27. package/dist/components/ps-document-zoom-image/ps-document-zoom-image.d.ts +1 -1
  28. package/dist/components/ps-document-zoom-image/ps-document-zoom-image.js +20 -3
  29. package/dist/components/ps-text/ps-text-props.d.ts +1 -0
  30. package/dist/components/ps-text/ps-text.d.ts +1 -1
  31. package/dist/components/ps-text/ps-text.js +2 -2
  32. package/dist/constants/ps-document-search-config.d.ts +19 -0
  33. package/dist/constants/ps-document-search-config.js +19 -0
  34. package/dist/hooks/use-ps-document-search-texts.d.ts +2 -0
  35. package/dist/hooks/use-ps-document-search-texts.js +26 -0
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.js +5 -0
  38. package/dist/interfaces/ps-document-highlight-rect.d.ts +5 -0
  39. package/dist/interfaces/ps-document-highlight-rect.js +1 -0
  40. package/dist/interfaces/ps-document-item.d.ts +3 -0
  41. package/dist/interfaces/ps-document-labels.d.ts +7 -0
  42. package/dist/interfaces/ps-document-normalized-text.d.ts +5 -0
  43. package/dist/interfaces/ps-document-normalized-text.js +1 -0
  44. package/dist/interfaces/ps-document-point.d.ts +4 -0
  45. package/dist/interfaces/ps-document-point.js +1 -0
  46. package/dist/interfaces/ps-document-search-labels.d.ts +9 -0
  47. package/dist/interfaces/ps-document-search-labels.js +1 -0
  48. package/dist/interfaces/ps-document-search-location.d.ts +4 -0
  49. package/dist/interfaces/ps-document-search-location.js +1 -0
  50. package/dist/interfaces/ps-document-search-match.d.ts +4 -0
  51. package/dist/interfaces/ps-document-search-match.js +1 -0
  52. package/dist/interfaces/ps-document-search-state.d.ts +4 -0
  53. package/dist/interfaces/ps-document-search-state.js +1 -0
  54. package/dist/interfaces/ps-document-text-box.d.ts +6 -0
  55. package/dist/interfaces/ps-document-text-box.js +1 -0
  56. package/dist/interfaces/ps-document-text-layer-word.d.ts +5 -0
  57. package/dist/interfaces/ps-document-text-layer-word.js +1 -0
  58. package/dist/interfaces/ps-document-text-layer.d.ts +6 -0
  59. package/dist/interfaces/ps-document-text-layer.js +1 -0
  60. package/dist/interfaces/ps-document-text-line.d.ts +4 -0
  61. package/dist/interfaces/ps-document-text-line.js +1 -0
  62. package/dist/interfaces/ps-document-text-segment.d.ts +4 -0
  63. package/dist/interfaces/ps-document-text-segment.js +1 -0
  64. package/dist/interfaces/ps-document-word-match.d.ts +3 -0
  65. package/dist/interfaces/ps-document-word-match.js +1 -0
  66. package/dist/services/ps-document-search-service.d.ts +40 -0
  67. package/dist/services/ps-document-search-service.js +201 -0
  68. package/dist/services/ps-document-search-style-service.d.ts +7 -0
  69. package/dist/services/ps-document-search-style-service.js +22 -0
  70. package/dist/services/ps-document-viewer-entry-service.js +1 -0
  71. package/dist/services/ps-document-zoom-service.d.ts +1 -0
  72. package/dist/services/ps-document-zoom-service.js +3 -0
  73. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ export * from './ps-document-highlight-layer';
2
+ export * from './ps-document-highlight-layer-props';
@@ -0,0 +1,2 @@
1
+ export * from './ps-document-highlight-layer';
2
+ export * from './ps-document-highlight-layer-props';
@@ -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,2 @@
1
+ import type { PsDocumentHighlightLayerProps } from './ps-document-highlight-layer-props';
2
+ export declare const PsDocumentHighlightLayer: ({ layer, search, size, }: PsDocumentHighlightLayerProps) => import("react").JSX.Element;
@@ -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,2 @@
1
+ export * from './ps-document-search-bar';
2
+ export * from './ps-document-search-bar-props';
@@ -0,0 +1,2 @@
1
+ export * from './ps-document-search-bar';
2
+ export * from './ps-document-search-bar-props';
@@ -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,2 @@
1
+ import type { PsDocumentSearchBarProps } from './ps-document-search-bar-props';
2
+ export declare const PsDocumentSearchBar: ({ activeIndex, labels, onChange, onClose, onNext, onPrevious, query, total, }: PsDocumentSearchBarProps) => import("react").JSX.Element;
@@ -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,2 @@
1
+ export * from './ps-document-search-step';
2
+ export * from './ps-document-search-step-props';
@@ -0,0 +1,2 @@
1
+ export * from './ps-document-search-step';
2
+ export * from './ps-document-search-step-props';
@@ -0,0 +1,7 @@
1
+ export interface PsDocumentSearchStepProps {
2
+ isDisabled: boolean;
3
+ isUp: boolean;
4
+ label: string;
5
+ onPress: () => void;
6
+ testID: string;
7
+ }
@@ -0,0 +1,2 @@
1
+ import type { PsDocumentSearchStepProps } from './ps-document-search-step-props';
2
+ export declare const PsDocumentSearchStep: ({ isDisabled, isUp, label, onPress, testID, }: PsDocumentSearchStepProps) => import("react").JSX.Element;
@@ -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: loaded.text }) }) }));
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(View, { style: { width: PsMobileControlConfig.controlHeights.default } })] }), _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, getItemLayout: (_data, position) => ({
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, status: entry.status }) })), scrollEnabled: !isZoomed, showsHorizontalScrollIndicator: false, testID: 'ps-document-viewer-list' })) }), PsDocumentViewerEntryService.hasStrip(items, entries) ? (_jsx(PsDocumentPageStrip, { entries: entries, index: index, onSelect: (position) => {
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: [_jsx(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 } }) }), isLoaded ? null : (_jsx(View, { pointerEvents: 'none', style: {
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',
@@ -12,6 +12,7 @@ export interface PsTextProps {
12
12
  isSelectable?: boolean;
13
13
  numberOfLines?: number;
14
14
  onPress?: () => void;
15
+ onTextLayout?: TextProps['onTextLayout'];
15
16
  style?: StyleProp<TextStyle>;
16
17
  testID?: string;
17
18
  tone?: PsTextTone;
@@ -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,2 @@
1
+ import type { PsDocumentItem } from '../interfaces/ps-document-item';
2
+ export declare const usePsDocumentSearchTexts: (items: PsDocumentItem[], isActive: boolean) => Record<string, string>;
@@ -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,5 @@
1
+ import type { PsDocumentTextBox } from './ps-document-text-box';
2
+ export interface PsDocumentHighlightRect {
3
+ box: PsDocumentTextBox;
4
+ isActive: boolean;
5
+ }
@@ -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,5 @@
1
+ export interface PsDocumentNormalizedText {
2
+ ends: number[];
3
+ starts: number[];
4
+ value: string;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface PsDocumentPoint {
2
+ x: number;
3
+ y: number;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export interface PsDocumentSearchLabels {
2
+ closeSearch: string;
3
+ matchCount: string;
4
+ nextMatch: string;
5
+ noMatches: string;
6
+ previousMatch: string;
7
+ search: string;
8
+ searchPlaceholder: string;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface PsDocumentSearchLocation {
2
+ entryIndex: number;
3
+ localIndex: number;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface PsDocumentSearchMatch {
2
+ end: number;
3
+ start: number;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface PsDocumentSearchState {
2
+ activeIndex: number;
3
+ query: string;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export interface PsDocumentTextBox {
2
+ height: number;
3
+ left: number;
4
+ top: number;
5
+ width: number;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { PsDocumentTextBox } from './ps-document-text-box';
2
+ export interface PsDocumentTextLayerWord {
3
+ box: PsDocumentTextBox;
4
+ text: string;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { PsDocumentTextLayerWord } from './ps-document-text-layer-word';
2
+ export interface PsDocumentTextLayer {
3
+ height: number;
4
+ width: number;
5
+ words: PsDocumentTextLayerWord[];
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface PsDocumentTextLine {
2
+ text: string;
3
+ y: number;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface PsDocumentTextSegment {
2
+ matchIndex: number | null;
3
+ text: string;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export interface PsDocumentWordMatch {
2
+ wordIndices: number[];
3
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-mobile-components",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "private": false,
5
5
  "description": "Native iOS (SwiftUI) and Android (Jetpack Compose) Ps components for Pallasoft mobile apps",
6
6
  "main": "./dist/index.js",