diff --git a/src/components/MarkersDisplay/Annotations/AnnotationLayerSelect.js b/src/components/MarkersDisplay/Annotations/AnnotationLayerSelect.js
index 013339ed..ea1fbdd4 100644
--- a/src/components/MarkersDisplay/Annotations/AnnotationLayerSelect.js
+++ b/src/components/MarkersDisplay/Annotations/AnnotationLayerSelect.js
@@ -1,25 +1,39 @@
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
parseExternalAnnotationPage,
parseExternalAnnotationResource
} from '@Services/annotations-parser';
-const AnnotationLayerSelect = ({ annotationLayers = [], duration = 0, setDisplayedAnnotationLayers }) => {
+const AnnotationLayerSelect = ({
+ canvasAnnotationLayers = [],
+ duration = 0,
+ setDisplayedAnnotationLayers,
+ setAutoScrollEnabled,
+ autoScrollEnabled,
+}) => {
const [selectedAnnotationLayers, setSelectedAnnotationLayers] = useState([]);
const [isOpen, setIsOpen] = useState(false);
const [selectedAll, setSelectedAll] = useState(false);
useEffect(() => {
- if (annotationLayers?.length > 0) {
+ // Reset state when Canvas changes
+ setSelectedAnnotationLayers([]);
+ setDisplayedAnnotationLayers([]);
+ setSelectedAll(false);
+ setIsOpen(false);
+
+ if (canvasAnnotationLayers?.length > 0) {
// Sort annotation sets alphabetically
- annotationLayers.sort((a, b) => a.label.localeCompare(b.label));
+ canvasAnnotationLayers.sort((a, b) => a.label.localeCompare(b.label));
// Select the first annotation set on page load
- findOrFetchandParseLinkedAnnotations(annotationLayers[0]);
+ findOrFetchandParseLinkedAnnotations(canvasAnnotationLayers[0]);
}
- }, [annotationLayers]);
+ }, [canvasAnnotationLayers]);
- const isSelected = (layer) => selectedAnnotationLayers.includes(layer.label);
+ const isSelected = useCallback((layer) => {
+ return selectedAnnotationLayers.includes(layer.label);
+ }, [selectedAnnotationLayers]);
const toggleDropdown = () => setIsOpen((prev) => !prev);
/**
@@ -68,7 +82,7 @@ const AnnotationLayerSelect = ({ annotationLayers = [], duration = 0, setDisplay
setSelectedAll(selectAllUpdated);
if (selectAllUpdated) {
await Promise.all(
- annotationLayers.map((annotationLayer) => {
+ canvasAnnotationLayers.map((annotationLayer) => {
findOrFetchandParseLinkedAnnotations(annotationLayer);
})
);
@@ -104,52 +118,72 @@ const AnnotationLayerSelect = ({ annotationLayers = [], duration = 0, setDisplay
setDisplayedAnnotationLayers((prev) => [...prev, annotationLayer]);
};
- return (
-
-
- {selectedAnnotationLayers.length > 0
- ? `${selectedAnnotationLayers.length} of ${annotationLayers.length} layers selected`
- : "Select Annotation layer(s)"}
-
▼
+ if (canvasAnnotationLayers?.length > 0) {
+ return (
+
+
+ {selectedAnnotationLayers.length > 0
+ ? `${selectedAnnotationLayers.length} of ${canvasAnnotationLayers.length} layers selected`
+ : "Select Annotation layer(s)"}
+ ▼
+
+ {isOpen && (
+
+ )}
+
+ { setAutoScrollEnabled(!autoScrollEnabled); }}
+ />
+
+ Auto-scroll with media
+
+
- {isOpen && (
-
- )}
-
- );
+ );
+ } else {
+ return null;
+ };
};
AnnotationLayerSelect.propTypes = {
- annotationLayers: PropTypes.array.isRequired,
+ canvasAnnotationLayers: PropTypes.array.isRequired,
duration: PropTypes.number.isRequired,
- setDisplayedAnnotationLayers: PropTypes.func.isRequired
+ setDisplayedAnnotationLayers: PropTypes.func.isRequired,
+ setAutoScrollEnabled: PropTypes.func.isRequired,
+ autoScrollEnabled: PropTypes.bool.isRequired,
};
export default AnnotationLayerSelect;
diff --git a/src/components/MarkersDisplay/Annotations/AnnotationLayerSelect.test.js b/src/components/MarkersDisplay/Annotations/AnnotationLayerSelect.test.js
new file mode 100644
index 00000000..aa2df515
--- /dev/null
+++ b/src/components/MarkersDisplay/Annotations/AnnotationLayerSelect.test.js
@@ -0,0 +1,484 @@
+import React from 'react';
+import { act, fireEvent, render, screen, within } from '@testing-library/react';
+import AnnotationLayerSelect from './AnnotationLayerSelect';
+import * as annotationParser from '@Services/annotations-parser';
+
+const annotationLayers = [
+ {
+ label: 'Unknown',
+ format: 'application/json',
+ url: 'http://example.com/manifestannotation-page/unknown.json',
+ },
+ {
+ label: 'Songs',
+ items: [{
+ id: 'songs-annotation-0',
+ canvasId: 'http://example.com/manifest/canvas/1',
+ motivation: ['supplementing', 'tagging'],
+ time: { start: 7, end: undefined },
+ value: [{ format: 'text/plain', purpose: ['supplementing'], value: 'Men singing' },
+ { format: 'text/plain', purpose: ['tagging'], value: 'Songs' }]
+ },
+ {
+ id: 'songs-annotation-1',
+ canvasId: 'http://example.com/manifest/canvas/1',
+ motivation: ['supplementing', 'tagging'],
+ time: { start: 25.32, end: 27.65 },
+ value: [{ format: 'text/plain', purpose: ['supplementing'], value: 'The Yale Glee Club singing "Mother of Men"' },
+ { format: 'text/plain', purpose: ['tagging'], value: 'Songs' }]
+ },
+ {
+ id: 'songs-annotation-2',
+ canvasId: 'http://example.com/manifest/canvas/1',
+ motivation: ['supplementing', 'tagging'],
+ time: { start: 29.54, end: 45.32 },
+ value: [{ format: 'text/plain', purpose: ['supplementing'], value: '
Subjects : Singing' },
+ { format: 'text/plain', purpose: ['tagging'], value: 'Songs' }]
+ }],
+ },
+ {
+ label: 'Texts',
+ format: 'application/json',
+ url: 'http://example.com/manifestannotation-page/texts.json',
+ items: [],
+ }
+];
+
+const linkedAnnotationLayers = [
+ {
+ canvasId: 'http://example.com/manifestcanvas/1',
+ format: 'text/vtt',
+ id: 'http://example.com/manifestcanvas/1/annotation-page/1/annotation/1',
+ label: 'Captions in English.vtt',
+ linkedResource: true,
+ motivation: ['supplementing'],
+ url: 'http://example.com/manifestfiles/captions-in-english.vtt',
+ items: [
+ {
+ id: 'http://example.com/manifest/canvas/1/annotation-page/1/annotation/1',
+ canvasId: 'http://example.com/manifest/canvas/1',
+ motivation: ['supplementing'],
+ time: { start: 7, end: undefined },
+ value: [{ format: 'text/plain', purpose: ['supplementing'], value: 'Men singing' }]
+ },
+ {
+ id: 'http://example.com/manifest/canvas/1/annotation-page/1/annotation/1',
+ canvasId: 'http://example.com/manifest/canvas/1',
+ motivation: ['supplementing'],
+ time: { start: 25.32, end: 27.65 },
+ value: [{ format: 'text/plain', purpose: ['supplementing'], value: 'The Yale Glee Club singing "Mother of Men"' }]
+ },
+ {
+ id: 'http://example.com/manifest/canvas/1/annotation-page/1/annotation/1',
+ canvasId: 'http://example.com/manifest/canvas/1',
+ motivation: ['supplementing'],
+ time: { start: 29.54, end: 45.32 },
+ value: [{ format: 'text/plain', purpose: ['supplementing'], value: '
Subjects : Singing' }]
+ }
+ ],
+ },
+ {
+ canvasId: 'http://example.com/manifestcanvas/1',
+ format: 'text/srt',
+ id: 'http://example.com/manifestcanvas/1/annotation-page/1/annotation/2',
+ label: 'Subtitle in English.srt',
+ linkedResource: true,
+ motivation: ['supplementing'],
+ url: 'http://example.com/manifestfiles/subtitles-in-english.srt',
+ },
+];
+
+const annotationPageResponse = {
+ "@context": "http://iiif.io/api/presentation/3/context.json",
+ "id": "http://example.com/manifestannotations/unknown.json",
+ "type": "AnnotationPage", "label": "Unknown",
+ "items": [
+ {
+ "@context": "http://www.w3.org/ns/anno.jsonld",
+ "id": "unknown-annotation-1.json",
+ "type": "Annotation",
+ "motivation": ["supplementing", "commenting"],
+ "body": [
+ { "type": "TextualBody", "value": "Savannah, GA", "format": "text/plain", "purpose": "commenting" },
+ { "type": "TextualBody", "value": "Unknown", "format": "text/plain", "purpose": "tagging" }
+ ],
+ "target": {
+ "source": {
+ "id": "http://example.com/manifest/canvas-1/canvas",
+ "type": "Canvas",
+ },
+ "selector": { "type": "PointSelector", "t": "2766.438533" }
+ }
+ },
+ {
+ "@context": "http://www.w3.org/ns/anno.jsonld",
+ "id": "unknown-annotation-2.json",
+ "type": "Annotation",
+ "motivation": ["supplementing", "commenting"],
+ "body": [
+ { "type": "TextualBody", "value": "A play that we used to play when we were children in Savannah.", "format": "text/plain", "purpose": "commenting" },
+ { "type": "TextualBody", "value": "Unknown", "format": "text/plain", "purpose": "tagging" }
+ ],
+ "target": {
+ "source": {
+ "id": "http://example.com/manifest/canvas-1/canvas",
+ "type": "Canvas",
+ },
+ "selector": {
+ "type": "FragmentSelector",
+ "conformsTo": "http://www.w3.org/TR/media-frags/",
+ "value": "t=2771.900826,2775.619835"
+ }
+ }
+ },
+ {
+ "@context": "http://www.w3.org/ns/anno.jsonld",
+ "id": "unknown-annotation-3.json",
+ "type": "Annotation",
+ "motivation": ["supplementing", "commenting"],
+ "body": [
+ { "type": "TextualBody", "value": "A ring play, just a ring play, a children's ring play", "format": "text/plain", "purpose": "commenting" },
+ { "type": "TextualBody", "value": "Unknown", "format": "text/plain", "purpose": "tagging" }
+ ],
+ "target": {
+ "source": {
+ "id": "http://example.com/manifest/canvas-1/canvas",
+ "type": "Canvas",
+ },
+ "selector": {
+ "type": "FragmentSelector",
+ "conformsTo": "http://www.w3.org/TR/media-frags/",
+ "value": "t=2779.493802,2782.438017"
+ }
+ }
+ }
+ ]
+};
+
+describe('AnnotationLayerSelect component', () => {
+ const setAutoScrollEnabledMock = jest.fn();
+ const setDisplayedAnnotationLayersMock = jest.fn();
+
+ test('displays nothing when there are no annotation layers', () => {
+ render(
);
+
+ expect(screen.queryByTestId('annotation-multi-select')).not.toBeInTheDocument();
+ });
+
+ describe('displays', () => {
+ beforeEach(() => {
+ render(
);
+ });
+
+ test('a multi-select box and a checkbox for auto-scroll on initial load', () => {
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ // Displays only multi select box and auto-scroll checkbox on initial load
+ expect(multiSelect.childNodes[0]).toHaveClass('ramp--annotations__multi-select-header');
+ expect(multiSelect.childNodes[0]).toHaveTextContent('1 of 3 layers selected▼');
+ expect(multiSelect.childNodes[1]).toHaveClass('ramp--annotations__scroll');
+ expect(multiSelect.childNodes[1]).toHaveTextContent('Auto-scroll with media');
+ });
+
+ test('a list of annotation layers on click', () => {
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+ expect(multiSelectHeader).toHaveTextContent('1 of 3 layers selected▼');
+
+ fireEvent.click(multiSelectHeader);
+
+ expect(multiSelect.childNodes[1].tagName).toEqual('UL');
+ expect(multiSelect.childNodes[1].childNodes.length).toEqual(4);
+ });
+
+ test('\'Show all Annotation layers\' option on top', () => {
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+ expect(multiSelectHeader).toHaveTextContent('1 of 3 layers selected▼');
+
+ fireEvent.click(multiSelectHeader);
+
+ expect(multiSelect.childNodes[1].childNodes.length).toEqual(4);
+ expect(multiSelect.childNodes[1].childNodes[0]).toHaveTextContent('Show all Annotation layers');
+ });
+
+ test('list of annotation layers in alphabetical order', () => {
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+ expect(multiSelectHeader).toHaveTextContent('1 of 3 layers selected▼');
+
+ fireEvent.click(multiSelectHeader);
+
+ const annotationList = multiSelect.childNodes[1];
+ expect(annotationList.childNodes.length).toEqual(4);
+ expect(annotationList.childNodes[1]).toHaveTextContent('Songs');
+ expect(annotationList.childNodes[2]).toHaveTextContent('Texts');
+ });
+
+ test('the first annotation layer as selected by default', () => {
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+ expect(multiSelectHeader).toHaveTextContent('1 of 3 layers selected▼');
+
+ fireEvent.click(multiSelectHeader);
+
+ const annotationList = multiSelect.childNodes[1];
+ expect(annotationList.childNodes.length).toEqual(4);
+ expect(annotationList.childNodes[1]).toHaveTextContent('Songs');
+ expect(within(annotationList.childNodes[1]).getByRole('checkbox')).toBeChecked();
+
+ expect(annotationList.childNodes[2]).toHaveTextContent('Texts');
+ expect(within(annotationList.childNodes[2]).getByRole('checkbox')).not.toBeChecked();
+ });
+ });
+
+ test('displays annotations options for linked resources', () => {
+ render(
);
+
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+ expect(multiSelectHeader).toHaveTextContent('1 of 2 layers selected▼');
+
+ fireEvent.click(multiSelectHeader);
+
+ expect(multiSelect.childNodes[1].tagName).toEqual('UL');
+ expect(multiSelect.childNodes[1].childNodes.length).toEqual(3);
+ });
+
+ describe('\'Show all Annotation layers\' option', () => {
+ test('is displayed when more than 1 annotation layer is present', () => {
+ render(
);
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+ expect(multiSelectHeader).toHaveTextContent('1 of 3 layers selected▼');
+
+ fireEvent.click(multiSelectHeader);
+
+ expect(multiSelect.childNodes[1].tagName).toEqual('UL');
+ expect(multiSelect.childNodes[1].childNodes.length).toEqual(4);
+ expect(screen.getByText('Show all Annotation layers')).toBeInTheDocument();
+ expect(screen.getByText('Songs')).toBeInTheDocument();
+ });
+
+ test('is not displayed when there is one annotation layer', () => {
+ render(
);
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+ expect(multiSelectHeader).toHaveTextContent('1 of 1 layers selected▼');
+
+ fireEvent.click(multiSelectHeader);
+
+ expect(multiSelect.childNodes[1].tagName).toEqual('UL');
+ expect(multiSelect.childNodes[1].childNodes.length).toEqual(1);
+ expect(screen.queryByText('Show all Annotation layers')).not.toBeInTheDocument();
+ expect(screen.getByText('Songs')).toBeInTheDocument();
+ });
+ });
+
+ describe('updates the annotation selection when', () => {
+ test('an annotation layer representing an external AnnotationPage is selected', async () => {
+ const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce({
+ status: 201,
+ ok: true,
+ json: jest.fn(() => { return annotationPageResponse; })
+ });
+
+ render(
);
+
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+
+ // Open the annotation layers list dropdown
+ fireEvent.click(multiSelectHeader);
+
+ const annotationLlist = multiSelect.childNodes[1];
+ // Check the annotation layer with label 'Texts' is not selected
+ expect(multiSelectHeader).toHaveTextContent('1 of 3 layers selected▼');
+ expect(annotationLlist.childNodes[3]).toHaveTextContent('Unknown');
+ expect(within(annotationLlist.childNodes[3]).getByRole('checkbox')).not.toBeChecked();
+
+ const checkBox = screen.queryAllByRole('checkbox')[3];
+
+ // Wrap in act() to ensure all state updates are processed before assertions are run.
+ // Select the 'Unknown' annotation layer from list
+ await act(() => {
+ fireEvent.click(checkBox);
+ });
+
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ expect(multiSelectHeader).toHaveTextContent('2 of 3 layers selected▼');
+ });
+
+ test('an annotation layer representing a linked WebVTT resource is selected', async () => {
+ const mockResponse =
+ 'WEBVTT\r\n\r\n1\r\n00:00:01.200 --> 00:00:21.000\n[music]\n\r\n2\r\n00:00:22.200 --> 00:00:26.600\nJust before lunch one day, a puppet show \nwas put on at school.\n\r\n3\r\n00:00:26.700 --> 00:00:31.500\nIt was called "Mister Bungle Goes to Lunch".\n\r\n4\r\n00:00:31.600 --> 00:00:34.500\nIt was fun to watch.\n\r\n5\r\n00:00:36.100 --> 00:00:41.300\nIn the puppet show, Mr. Bungle came to the \nboys\' room on his way to lunch.\n';
+ const fetchWebVTT = jest.spyOn(global, 'fetch').mockResolvedValueOnce({
+ status: 200,
+ ok: true,
+ headers: { get: jest.fn(() => 'text/vtt') },
+ text: jest.fn(() => mockResponse),
+ });
+ const parseExternalAnnotationResourceMock = jest
+ .spyOn(annotationParser, 'parseExternalAnnotationResource');
+
+ render(
);
+
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+
+ // Open the annotation layers list dropdown
+ fireEvent.click(multiSelectHeader);
+
+ const annotationLlist = multiSelect.childNodes[1];
+
+ // Check the annotation layer with label 'Subtitle in English.srt' is not selected
+ expect(multiSelectHeader).toHaveTextContent('1 of 2 layers selected▼');
+ expect(annotationLlist.childNodes[2]).toHaveTextContent('Subtitle in English.srt');
+ expect(within(annotationLlist.childNodes[2]).getByRole('checkbox')).not.toBeChecked();
+
+ const checkBox = screen.queryAllByRole('checkbox')[2];
+
+ // Wrap in act() to ensure all state updates are processed before assertions are run.
+ // Select the 'Subtitle in English.srt' annotation layer from list
+ await act(() => {
+ fireEvent.click(checkBox);
+ });
+
+ expect(parseExternalAnnotationResourceMock).toHaveBeenCalledTimes(1);
+ expect(fetchWebVTT).toHaveBeenCalledTimes(1);
+ expect(multiSelectHeader).toHaveTextContent('2 of 2 layers selected▼');
+ });
+
+ test('\'Show all Annotation layers\' option is selected', async () => {
+ jest.spyOn(annotationParser, 'parseExternalAnnotationPage')
+ .mockResolvedValue([{}]);
+
+ render(
);
+
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+ const multiSelect = screen.getByTestId('annotation-multi-select');
+ const multiSelectHeader = multiSelect.childNodes[0];
+
+ // Open the annotation layers list dropdown
+ fireEvent.click(multiSelectHeader);
+
+ const annotationLlist = multiSelect.childNodes[1];
+ // Check 'Show all Annotation layers' option is not selected
+ expect(multiSelectHeader).toHaveTextContent('1 of 3 layers selected▼');
+ expect(annotationLlist.childNodes[0]).toHaveTextContent('Show all Annotation layers');
+ expect(within(annotationLlist.childNodes[0]).getByRole('checkbox')).not.toBeChecked();
+
+ const checkBox = screen.queryAllByRole('checkbox')[0];
+
+ // Wrap in act() to ensure all state updates are processed before assertions are run.
+ // Select the 'Show all Annotation layers' annotation layer from list
+ await act(() => {
+ fireEvent.click(checkBox);
+ });
+
+ // Dropdown list for annotation layers collapses
+ expect(screen.queryByText('Show all Annotation layers')).not.toBeInTheDocument();
+ expect(screen.queryByText('Songs')).not.toBeInTheDocument();
+ // Text in the select box shows all layers are selected
+ expect(multiSelectHeader).toHaveTextContent('3 of 3 layers selected▼');
+ });
+ });
+
+ describe('\'Auto-scroll with media\' checkbox', () => {
+ beforeEach(() => {
+ render(
);
+ });
+
+ test('is displayed checked on initial load', () => {
+ expect(screen.queryByTestId('annotation-multi-select')).toBeInTheDocument();
+
+ const checkBox = screen.getByTestId('annotation-multi-select').childNodes[1];
+ expect(checkBox).toHaveClass('ramp--annotations__scroll');
+ expect(checkBox).toHaveTextContent('Auto-scroll with media');
+ expect(within(checkBox).getByRole('checkbox')).toBeChecked();
+ });
+
+ test('calls \'setAutoScrollEnabled\' when clicked', () => {
+ const checkBox = screen.getByTestId('annotation-multi-select').childNodes[1];
+ expect(checkBox).toHaveClass('ramp--annotations__scroll');
+ expect(checkBox).toHaveTextContent('Auto-scroll with media');
+
+ fireEvent.click(within(checkBox).getByRole('checkbox'));
+
+ expect(setAutoScrollEnabledMock).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/src/components/MarkersDisplay/Annotations/AnnotationRow.js b/src/components/MarkersDisplay/Annotations/AnnotationRow.js
index b71a461d..e997cba1 100644
--- a/src/components/MarkersDisplay/Annotations/AnnotationRow.js
+++ b/src/components/MarkersDisplay/Annotations/AnnotationRow.js
@@ -1,15 +1,33 @@
-import React, { useCallback, useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
-import { timeToHHmmss } from '@Services/utility-helpers';
+import cx from 'classnames';
+import { autoScroll, timeToHHmmss } from '@Services/utility-helpers';
import { useAnnotations, useMediaPlayer } from '@Services/ramp-hooks';
+import { SUPPORTED_MOTIVATIONS } from '@Services/annotations-parser';
-const AnnotationRow = ({ annotation, displayMotivations }) => {
+const AnnotationRow = ({
+ annotation,
+ displayMotivations,
+ autoScrollEnabled,
+ containerRef,
+ displayedAnnotations
+}) => {
const { id, canvasId, motivation, time, value } = annotation;
- const { start, end } = time;
- const { player } = useMediaPlayer();
- const { checkCanvas } = useAnnotations({ canvasId });
+ const [isActive, setIsActive] = useState(false);
+ const [showLongText, setShowLongText] = useState(false);
+
+ const { player, currentTime } = useMediaPlayer();
+ const { checkCanvas, inPlayerRange } = useAnnotations({
+ canvasId,
+ startTime: time?.start,
+ endTime: time?.end,
+ currentTime,
+ displayedAnnotations
+ });
+
+ const annotationRef = useRef(null);
/**
* Display only the annotations with at least one of the specified motivations
@@ -24,25 +42,42 @@ const AnnotationRow = ({ annotation, displayMotivations }) => {
}, [annotation]);
/**
- * Seek the player to;
+ * When there multiple annotations in the same time range, auto-scroll to
+ * the annotation with the start time that is closest to the current time
+ * of the player.
+ * This allows a better user experience when auto-scroll is enabled during playback,
+ * and there are multiple annotations that falls within the same time range.
+ */
+ useEffect(() => {
+ inPlayerRange ? setIsActive(true) : setIsActive(false);
+ if (autoScrollEnabled && inPlayerRange) {
+ autoScroll(annotationRef.current, containerRef, true);
+ }
+ }, [inPlayerRange]);
+
+ /**
+ * Click event handler for annotations displayed.
+ * An annotation can have links embedded in the text; and the click event's
+ * target is a link, then open the link in the same page.
+ * If the click event's target is the text or the timestamp of the
+ * annotation, then seek the player to;
* - start time of an Annotation with a time range
- * - timestamp of an Annotation with a single time-point
- * on click event on each Annotation
+ * - timestamp of an Annotation with a single time-point.
*/
const handleOnClick = useCallback((e) => {
e.preventDefault();
checkCanvas();
- const currentTime = start;
+ const currTime = time?.start;
if (player) {
const { start, end } = player.targets[0];
switch (true) {
- case currentTime >= start && currentTime <= end:
- player.currentTime(currentTime);
+ case currTime >= start && currTime <= end:
+ player.currentTime(currTime);
break;
- case currentTime < start:
+ case currTime < start:
player.currentTime(start);
break;
- case currentTime > end:
+ case currTime > end:
player.currentTime(end);
break;
}
@@ -51,35 +86,62 @@ const AnnotationRow = ({ annotation, displayMotivations }) => {
// Annotations with purpose tagging are displayed as tags next to time
const tags = value.filter((v) => v.purpose.includes('tagging'));
- // Annotations with purpose commenting/supplementing are displayed as text
+ // Annotations with purpose commenting/supplementing/transcribing are displayed as text
const texts = value.filter(
- (v) => v.purpose.includes('commenting') || v.purpose.includes('supplementing')
+ (v) => SUPPORTED_MOTIVATIONS.some(m => v.purpose.includes(m))
);
+ /**
+ * Click event handler for the 'Show more'/'Show less' button for
+ * each annotation displayed.
+ */
+ const handleShowMoreLessClick = () => {
+ if (!showLongText) {
+ // Show all lines on click of 'Show more' button
+ const hiddenTexts = annotationRef.current
+ .querySelectorAll('.ramp--annotations__annotation-text.hidden');
+ hiddenTexts.forEach((text) => {
+ text.classList.remove('hidden');
+ });
+ } else {
+ // Show only the first 6 lines on click of 'Show less' button
+ const allTexts = annotationRef.current
+ .querySelectorAll('.ramp--annotations__annotation-text');
+ allTexts.forEach((text, index) => {
+ if (index > 5) {
+ text.classList.add('hidden');
+ }
+ });
+ }
+ setShowLongText(!showLongText);
+ };
+
if (canDisplay) {
return (
- {start != undefined && (
+ {time?.start != undefined && (
- {timeToHHmmss(start, true)}
+ data-testid="annotation-start-time">
+ {timeToHHmmss(time?.start, true, true)}
)}
- {end != undefined && (
+ {time?.end != undefined && (
- {` - ${timeToHHmmss(end, true)}`}
+ data-testid="annotation-end-time">
+ {` - ${timeToHHmmss(time?.end, true, true)}`}
)}
@@ -89,6 +151,7 @@ const AnnotationRow = ({ annotation, displayMotivations }) => {
{tag.value}
@@ -96,15 +159,30 @@ const AnnotationRow = ({ annotation, displayMotivations }) => {
})}
- {texts?.length > 0 && texts.map((text, index) => {
- return (
-
+ {displayMotivations?.length > 0
+ ? `No Annotations were found with ${displayMotivations.join('/')} motivation.`
+ : 'No Annotations were found for the selected layer(s).'}
+
+ );
+ }
}
- }, [annotations, canvasIndex]);
+ }, [hasDisplayAnnotations, displayedAnnotations, isLoading]);
if (canvasAnnotationLayers?.length > 0) {
return (
@@ -52,33 +157,20 @@ const AnnotationsDisplay = ({ annotations, canvasIndex, duration, displayMotivat
data-testid="annotations-display">