diff --git a/README.md b/README.md index c834853f..54de8bc3 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ In the following table the list of parameters that can be provided to the `pdf_v | viewer_align | The alignment of the PDF viewer within its container. Can be `"center"` (default), `"left"`, or `"right"`. | | show_page_separator | Whether to show a horizontal separator line between PDF pages. Defaults to `True`. | | scroll_to_page | Scroll to a specific page when the component is rendered. The parameter is an integer, which represent the positional value of the page. E.g. 1, will be the first page. Default is None. Require ints and ignores the parameters below zero. | -| scroll_to_annotation | Scroll to a specific annotation when the component is rendered. The parameter is an integer, which represent the positional value of the annotation. E.g. 1, will be the first annotation. Default is None (don't scroll). Mutually exclusive with `scroll_to_page`. Raise an exception if used with `scroll_to_page` | +| scroll_to_annotation | Scroll to a specific annotation when the component is rendered. The parameter is a 1-based positional integer referring to the annotation's order in the `annotations` list, independent of any `id` field set on the annotation. E.g. 1 scrolls to the first annotation. Default is None (don't scroll). Requires an int; values below 1 are silently coerced to None. Mutually exclusive with `scroll_to_page`. Raises an exception if used with `scroll_to_page`. | | scroll_behavior | The scrolling behavior when navigating to a page or annotation. Can be `"smooth"` (animated scroll) or `"instant"` (immediate jump). Defaults to `"smooth"`. | | on_annotation_click | Callback function that is called when an annotation is clicked. The function receives the annotation as a parameter. | diff --git a/requirements.txt b/requirements.txt index 640c3eb1..05333d4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -streamlit -bump-my-version +streamlit==1.40.1 +bump-my-version==1.3.0 tornado>=6.5 # not directly required, pinned by Snyk to avoid a vulnerability requests>=2.32.4 # not directly required, pinned by Snyk to avoid a vulnerability protobuf>=4.25.8 # not directly required, pinned by Snyk to avoid a vulnerability \ No newline at end of file diff --git a/streamlit_pdf_viewer/__init__.py b/streamlit_pdf_viewer/__init__.py index 63aa624c..e6acbb5b 100644 --- a/streamlit_pdf_viewer/__init__.py +++ b/streamlit_pdf_viewer/__init__.py @@ -59,7 +59,7 @@ def pdf_viewer( :param viewer_align: The alignment of the PDF viewer in the container. Can be "center", "left", or "right". Defaults to "center". :param show_page_separator: Whether to show a separator between pages. Defaults to True. :param scroll_to_page: Scroll to a specific page in the PDF. The parameter is an integer, which represent the positional value of the page. E.g. 1, will be the first page. Defaults to None. - :param scroll_to_annotation: Scroll to a specific annotation in the PDF. The parameter is an integer, which represent the positional value of the annotation. E.g. 1, will be the first annotation. Defaults to None. + :param scroll_to_annotation: Scroll to a specific annotation in the PDF. The parameter is a 1-based positional integer referring to the order of the annotation in the `annotations` list, independent of any `id` field on the annotation itself. E.g. 1 scrolls to the first annotation. Values below 1 are silently coerced to None. Defaults to None. :param scroll_behavior: The scrolling behavior when navigating to a page or annotation. Can be "smooth" (animated scroll) or "instant" (immediate jump). Defaults to "smooth". :param on_annotation_click: A callback function that will be called when an annotation is clicked. The function should accept a single argument, which is the annotation that was clicked. Defaults to None. :param allow_clickable_annotations_with_text_rendering: When True, annotations remain clickable even when render_text is enabled. Note that text selection will not work through annotation areas. Defaults to False. diff --git a/streamlit_pdf_viewer/frontend/src/PdfViewer.vue b/streamlit_pdf_viewer/frontend/src/PdfViewer.vue index f9b69de5..1799eb04 100644 --- a/streamlit_pdf_viewer/frontend/src/PdfViewer.vue +++ b/streamlit_pdf_viewer/frontend/src/PdfViewer.vue @@ -149,6 +149,8 @@ export default { annotation.id = `${annotation.id || annotationIndex}` annotationDiv.id = `annotation-${annotation.id}`; annotationDiv.setAttribute("data-index", annotation.id); + // 0-based positional index, independent of user-supplied annotation.id. Used by scroll_to_annotation. + annotationDiv.setAttribute("data-position", annotationIndex); annotationDiv.style.position = 'absolute'; annotationDiv.style.left = `${annotation.x * scale}px`; annotationDiv.style.top = `${annotation.y * scale}px`; @@ -350,7 +352,8 @@ export default { page.scrollIntoView({behavior}); } } else if (props.args.scroll_to_annotation) { - const annotation = document.querySelector(`[id^="annotation-"][data-index="${props.args.scroll_to_annotation}"]`); + // Public API is 1-based; data-position is 0-based. + const annotation = document.querySelector(`[id^="annotation-"][data-position="${props.args.scroll_to_annotation - 1}"]`); if (annotation) { annotation.scrollIntoView({behavior, block: "center"}); } diff --git a/tests/test_scroll_behavior.py b/tests/test_scroll_behavior.py index e0eba795..5f1f60e6 100644 --- a/tests/test_scroll_behavior.py +++ b/tests/test_scroll_behavior.py @@ -43,3 +43,34 @@ def test_rejects_invalid_value(self, mock_component): def test_rejects_empty_string(self, mock_component): with pytest.raises(ValueError, match="scroll_behavior"): pdf_viewer(DUMMY_PDF, scroll_behavior="") + + +@patch("streamlit_pdf_viewer._component_func", return_value=None) +class TestScrollToAnnotationContract: + """Python forwards scroll_to_annotation as 1-based; the frontend converts to its + own 0-based data-position attribute. Python's job is just pass-through plus the + pre-existing < 1 → None coercion.""" + + def test_one_passes_through(self, mock_component): + pdf_viewer(DUMMY_PDF, scroll_to_annotation=1) + assert mock_component.call_args[1]["scroll_to_annotation"] == 1 + + def test_arbitrary_n_passes_through(self, mock_component): + pdf_viewer(DUMMY_PDF, scroll_to_annotation=5) + assert mock_component.call_args[1]["scroll_to_annotation"] == 5 + + def test_none_passes_through(self, mock_component): + pdf_viewer(DUMMY_PDF) + assert mock_component.call_args[1]["scroll_to_annotation"] is None + + def test_zero_is_coerced_to_none(self, mock_component): + pdf_viewer(DUMMY_PDF, scroll_to_annotation=0) + assert mock_component.call_args[1]["scroll_to_annotation"] is None + + def test_negative_is_coerced_to_none(self, mock_component): + pdf_viewer(DUMMY_PDF, scroll_to_annotation=-3) + assert mock_component.call_args[1]["scroll_to_annotation"] is None + + def test_combined_with_scroll_to_page_raises(self, mock_component): + with pytest.raises(ValueError, match="cannot be used together"): + pdf_viewer(DUMMY_PDF, scroll_to_page=1, scroll_to_annotation=1)