Skip to content

Commit 35c473a

Browse files
authored
feat: add "active" state to highlight on PdfViewer (#259)
1 parent d20665e commit 35c473a

8 files changed

Lines changed: 335 additions & 78 deletions

File tree

packages/discovery-react-components/src/components/DocumentPreview/components/PdfViewerHighlight/PdfViewerHighlight.tsx

Lines changed: 103 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import React, { FC, useMemo } from 'react';
1+
import React, { FC, useMemo, useEffect, useRef } from 'react';
22
import cx from 'classnames';
33
import { settings } from 'carbon-components';
44
import { QueryResult } from 'ibm-watson/discovery/v2';
55
import { ProcessedDoc } from 'utils/document';
6-
import { TextMappings } from '../../types';
6+
import { Bbox, TextMappings } from '../../types';
77
import { PdfDisplayProps } from '../PdfViewer/types';
88
import { PdfRenderedText } from '../PdfViewer/PdfViewerTextLayer';
99
import { ExtractedDocumentInfo } from './utils/common/documentUtils';
1010
import { Highlighter } from './utils/Highlighter';
11-
import { HighlightProps } from './types';
11+
import { HighlightProps, HighlightShape } from './types';
1212

1313
type Props = PdfDisplayProps &
1414
HighlightProps & {
@@ -34,10 +34,12 @@ type Props = PdfDisplayProps &
3434
const PdfViewerHighlight: FC<Props> = ({
3535
className,
3636
highlightClassName,
37+
activeHighlightClassName,
3738
document,
3839
parsedDocument,
3940
page,
4041
highlights,
42+
activeIds,
4143
pdfRenderedText,
4244
scale,
4345
_useHtmlBbox = true,
@@ -54,46 +56,78 @@ const PdfViewerHighlight: FC<Props> = ({
5456

5557
const { textDivs } = pdfRenderedText || {};
5658

57-
const highlightBoxes = useMemo(() => {
59+
const highlightShapes = useMemo(() => {
5860
highlighter?.setTextContentDivs(textDivs);
59-
return highlights.map(highlight => {
60-
return highlighter?.getHighlight(highlight);
61-
});
61+
return highlighter
62+
? highlights.map(highlight => {
63+
return highlighter.getHighlight(highlight);
64+
})
65+
: [];
6266
}, [highlighter, highlights, textDivs]);
6367

68+
const highlightDivRef = useRef<HTMLDivElement | null>(null);
69+
useScrollIntoActiveHighlight(highlightDivRef, highlightShapes, activeIds);
70+
71+
return (
72+
<div
73+
ref={highlightDivRef}
74+
className={cx(`${settings.prefix}--document-preview-pdf-viewer-highlight`, className)}
75+
>
76+
{highlightShapes.map(shape => {
77+
const active = activeIds?.includes(shape.highlightId);
78+
return (
79+
<Highlight
80+
key={shape.highlightId}
81+
className={highlightClassName}
82+
activeClassName={activeHighlightClassName}
83+
shape={shape}
84+
scale={scale}
85+
active={active}
86+
/>
87+
);
88+
})}
89+
</div>
90+
);
91+
};
92+
93+
const Highlight: FC<{
94+
className?: string;
95+
activeClassName?: string;
96+
shape: HighlightShape;
97+
scale: number;
98+
active?: boolean;
99+
}> = ({ className, activeClassName, shape, scale, active }) => {
64100
return (
65-
<div className={cx(`${settings.prefix}--document-preview-pdf-viewer-highlight`, className)}>
66-
{highlightBoxes.map((hl, hlIndex) => {
101+
<div data-highlight-id={shape.highlightId}>
102+
{shape?.boxes.map(item => {
67103
return (
68-
<React.Fragment key={`k-${hlIndex}`}>
69-
{hl?.boxes.map((item, index) => {
70-
const padding = 0;
71-
const [left, top, right, bottom] = item.bbox;
72-
return (
73-
<div
74-
key={`${left}${top}${right}${bottom}_${index}`}
75-
className={cx(
76-
`${settings.prefix}--document-preview-pdf-viewer-highlight--item`,
77-
highlightClassName,
78-
hl.className
79-
)}
80-
style={{
81-
left: `${(left - padding) * scale}px`,
82-
top: `${(top - padding) * scale}px`,
83-
width: `${(right - left + padding) * scale}px`,
84-
height: `${(bottom - top + padding) * scale}px`
85-
}}
86-
data-testid="highlight"
87-
/>
88-
);
89-
})}
90-
</React.Fragment>
104+
<div
105+
key={`${item.bbox[0].toFixed(2)}_${item.bbox[1].toFixed(2)}`}
106+
className={cx(
107+
`${settings.prefix}--document-preview-pdf-viewer-highlight--item`,
108+
className,
109+
shape.className,
110+
active && `${settings.prefix}--document-preview-pdf-viewer-highlight--item--active`,
111+
active && activeClassName
112+
)}
113+
style={{ ...getPositionStyle(item.bbox, scale) }}
114+
/>
91115
);
92116
})}
93117
</div>
94118
);
95119
};
96120

121+
function getPositionStyle(bbox: Bbox, scale: number, padding: number = 0) {
122+
const [left, top, right, bottom] = bbox;
123+
return {
124+
left: `${(left - padding) * scale}px`,
125+
top: `${(top - padding) * scale}px`,
126+
width: `${(right - left + padding) * scale}px`,
127+
height: `${(bottom - top + padding) * scale}px`
128+
};
129+
}
130+
97131
const useHighlighter = ({
98132
document,
99133
textMappings,
@@ -127,4 +161,41 @@ const useHighlighter = ({
127161
}, [document, isReady, pageNum, pdfRenderedText, processedDoc, textMappings]);
128162
};
129163

164+
function useScrollIntoActiveHighlight(
165+
highlightDivRef: React.MutableRefObject<HTMLDivElement | null>,
166+
shapes: HighlightShape[],
167+
activeIds: string[] | undefined
168+
) {
169+
useEffect(() => {
170+
if (!highlightDivRef.current) {
171+
return;
172+
}
173+
174+
const activeShape = shapes.find(
175+
shape => shape?.highlightId && activeIds?.includes(shape.highlightId)
176+
);
177+
if (activeShape) {
178+
let timer: NodeJS.Timeout | null = setTimeout(() => {
179+
timer = null;
180+
181+
const highlightDiv = highlightDivRef.current;
182+
if (!highlightDiv) return;
183+
184+
const highlightElm = highlightDiv?.querySelector(
185+
`[data-highlight-id=${activeShape.highlightId}]`
186+
);
187+
highlightElm?.firstElementChild?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
188+
}, 0);
189+
190+
// cleanup timeout
191+
return () => {
192+
if (timer) {
193+
clearTimeout(timer);
194+
}
195+
};
196+
}
197+
return;
198+
}, [activeIds, highlightDivRef, shapes]);
199+
}
200+
130201
export default PdfViewerHighlight;

packages/discovery-react-components/src/components/DocumentPreview/components/PdfViewerHighlight/PdfViewerWithHighlight.stories.tsx

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import React, { useCallback, useMemo, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { storiesOf } from '@storybook/react';
3-
import { withKnobs, radios, number } from '@storybook/addon-knobs';
3+
import { withKnobs, radios, number, select } from '@storybook/addon-knobs';
44
import { action } from '@storybook/addon-actions';
55
import PdfViewerWithHighlight from './PdfViewerWithHighlight';
66
import { flatten } from 'lodash';
77
import { DocumentFieldHighlight } from './types';
88
import './PdfViewerWithHighlight.stories.scss';
9+
import { nonEmpty } from 'utils/nonEmpty';
10+
import { getDocFieldValue } from './utils/common/documentUtils';
911

1012
import { document as doc } from 'components/DocumentPreview/__fixtures__/Art Effects.pdf';
1113
import document from 'components/DocumentPreview/__fixtures__/Art Effects Koya Creative Base TSA 2008.pdf.json';
@@ -14,7 +16,6 @@ import { document as docJa } from 'components/DocumentPreview/__fixtures__/Disco
1416
import documentJa from 'components/DocumentPreview/__fixtures__/DiscoComponents-ja_document.json';
1517

1618
import PDFJS from 'pdfjs-dist';
17-
import { getDocFieldValue } from './utils/common/documentUtils';
1819
(PDFJS as any).cMapUrl = './node_modules/pdfjs-dist/cmaps/';
1920
(PDFJS as any).cMapPacked = true;
2021

@@ -39,7 +40,37 @@ const zoomKnob = {
3940
defaultValue: '1'
4041
};
4142

42-
const EMPTY: never[] = [];
43+
const EMPTY: DocumentFieldHighlight[] = [];
44+
const HIGHLIGHT_COMPANIES: DocumentFieldHighlight[] = [
45+
{ id: 'highlight0', field: 'text', fieldIndex: 0, location: { end: 404, begin: 385 } },
46+
{ id: 'highlight1', field: 'text', fieldIndex: 0, location: { end: 436, begin: 419 } },
47+
{ id: 'highlight2', field: 'text', fieldIndex: 0, location: { end: 10334, begin: 10319 } }
48+
];
49+
50+
const HIGHLIGHT_CUSTOMER_GROUPS: DocumentFieldHighlight[] = [
51+
{ id: 'highlight0', field: 'text', fieldIndex: 0, location: { end: 3495, begin: 3481 } },
52+
{ id: 'highlight1', field: 'text', fieldIndex: 0, location: { end: 5566, begin: 5552 } },
53+
{ id: 'highlight2', field: 'text', fieldIndex: 0, location: { end: 8576, begin: 8562 } },
54+
{ id: 'highlight3', field: 'text', fieldIndex: 0, location: { end: 8975, begin: 8961 } },
55+
{ id: 'highlight4', field: 'text', fieldIndex: 0, location: { end: 68800, begin: 68786 } },
56+
{ id: 'highlight5', field: 'text', fieldIndex: 0, location: { end: 135747, begin: 135733 } },
57+
{ id: 'highlight6', field: 'text', fieldIndex: 0, location: { end: 139911, begin: 139897 } }
58+
];
59+
60+
const highlightKnob = {
61+
label: 'Highlights',
62+
options: {
63+
empty: 'empty',
64+
'3 companies': 'companies',
65+
'7 customer groups': 'customerGroups'
66+
},
67+
defaultValue: 'empty',
68+
data: {
69+
empty: EMPTY,
70+
companies: HIGHLIGHT_COMPANIES,
71+
customerGroups: HIGHLIGHT_CUSTOMER_GROUPS
72+
}
73+
};
4374

4475
const WithTextSelection: typeof PdfViewerWithHighlight = props => {
4576
const [selectedField, setSelectedField] = useState<string | null>('text|||0');
@@ -111,6 +142,7 @@ const WithTextSelection: typeof PdfViewerWithHighlight = props => {
111142
const fieldText = getDocFieldValue(document, selectedFieldName, selectedFieldIndex);
112143

113144
const highlight: DocumentFieldHighlight = {
145+
id: 'highlight',
114146
field: selectedFieldName,
115147
fieldIndex: selectedFieldIndex,
116148
location: { begin: Math.min(begin, end), end: Math.max(begin, end) },
@@ -119,22 +151,30 @@ const WithTextSelection: typeof PdfViewerWithHighlight = props => {
119151
setHighlights([highlight]);
120152
};
121153

154+
const activeIds = useMemo(() => highlights.map(hl => hl.id).filter(nonEmpty), [highlights]);
155+
122156
return (
123157
<div className="withTextSelection">
124-
<PdfViewerWithHighlight {...props} highlights={highlights} highlightClassName="highlight" />
158+
<PdfViewerWithHighlight
159+
{...props}
160+
highlights={highlights}
161+
activeIds={activeIds}
162+
highlightClassName="highlight"
163+
/>
125164
<div className="rightPane">
126165
<h6>
127166
<label htmlFor="field_select">Select field</label>
128167
</h6>
129168
<p>
130169
{/* eslint-disable-next-line jsx-a11y/no-onchange*/}
131-
<select name="field_select" id="field_select" onChange={handleOnChangeField}>
170+
<select
171+
name="field_select"
172+
id="field_select"
173+
value={selectedField || ''}
174+
onChange={handleOnChangeField}
175+
>
132176
{fieldOptions.map(option => (
133-
<option
134-
key={option.value}
135-
value={option.value}
136-
selected={option.value === selectedField}
137-
>
177+
<option key={option.value} value={option.value}>
138178
{option.label}
139179
</option>
140180
))}
@@ -159,16 +199,41 @@ storiesOf('DocumentPreview/components/PdfViewerWithHighlight', module)
159199
const page = number(pageKnob.label, pageKnob.defaultValue, pageKnob.options);
160200
const zoom = radios(zoomKnob.label, zoomKnob.options, zoomKnob.defaultValue);
161201
const scale = parseFloat(zoom);
202+
const highlights = select(
203+
highlightKnob.label,
204+
highlightKnob.options,
205+
highlightKnob.defaultValue
206+
);
207+
const activeId = number('Active highlight index', 0);
162208
const setLoadingAction = action('setLoading');
209+
const setCurrentPageAction = action('setCurrentPage');
210+
211+
const [currentPage, setCurrentPage] = useState(0);
212+
useEffect(() => {
213+
setCurrentPage(page);
214+
}, [page]);
215+
const handleSetCurrentPage = useCallback((p: number) => {
216+
setCurrentPageAction(p);
217+
setCurrentPage(p);
218+
}, []);
219+
220+
const [activeIds, setActiveIds] = useState<string[]>([]);
221+
useEffect(() => {
222+
const items = highlightKnob.data[highlights];
223+
const item = items[activeId];
224+
setActiveIds(item ? [item.id] : []);
225+
}, [activeId, highlights]);
163226

164227
return (
165228
<PdfViewerWithHighlight
166229
file={atob(doc)}
167-
page={page}
230+
page={currentPage}
168231
scale={scale}
169232
setLoading={setLoadingAction}
170233
document={document}
171-
highlights={EMPTY}
234+
highlights={highlightKnob.data[highlights]}
235+
activeIds={activeIds}
236+
setCurrentPage={handleSetCurrentPage}
172237
/>
173238
);
174239
})
@@ -177,6 +242,7 @@ storiesOf('DocumentPreview/components/PdfViewerWithHighlight', module)
177242
const zoom = radios(zoomKnob.label, zoomKnob.options, zoomKnob.defaultValue);
178243
const scale = parseFloat(zoom);
179244
const setLoadingAction = action('setLoading');
245+
const setCurrentPage = action('setCurrentPage');
180246

181247
return (
182248
<WithTextSelection
@@ -186,6 +252,7 @@ storiesOf('DocumentPreview/components/PdfViewerWithHighlight', module)
186252
setLoading={setLoadingAction}
187253
document={document}
188254
highlights={EMPTY}
255+
setCurrentPage={setCurrentPage}
189256
/>
190257
);
191258
})
@@ -194,6 +261,7 @@ storiesOf('DocumentPreview/components/PdfViewerWithHighlight', module)
194261
const zoom = radios(zoomKnob.label, zoomKnob.options, zoomKnob.defaultValue);
195262
const scale = parseFloat(zoom);
196263
const setLoadingAction = action('setLoading');
264+
const setCurrentPage = action('setCurrentPage');
197265

198266
return (
199267
<WithTextSelection
@@ -203,6 +271,7 @@ storiesOf('DocumentPreview/components/PdfViewerWithHighlight', module)
203271
setLoading={setLoadingAction}
204272
document={documentJa}
205273
highlights={EMPTY}
274+
setCurrentPage={setCurrentPage}
206275
/>
207276
);
208277
});

0 commit comments

Comments
 (0)