From baf3c4a24b1ea2fc5c4c0c1ab6b853faae286afa Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Tue, 13 Feb 2024 22:37:21 -0800 Subject: [PATCH 01/30] update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b7c5b499..774efb30 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ html/ # VSCode .vscode/ + +dist/tsconfig.tsbuildinfo tmp +.env.local From 15ac0ec9a75926309953d6482f0f9cffe0ff7d4f Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Thu, 22 Feb 2024 12:55:12 -0800 Subject: [PATCH 02/30] add plugins to Viewer --- docs/components/DynamicImports/Viewer.tsx | 4 ++++ src/components/Viewer/Player/Player.test.tsx | 2 ++ src/components/Viewer/Viewer/Viewer.tsx | 18 ++++++++++++++++++ src/components/Viewer/index.tsx | 4 ++++ src/context/viewer-context.tsx | 6 ++++++ 5 files changed, 34 insertions(+) diff --git a/docs/components/DynamicImports/Viewer.tsx b/docs/components/DynamicImports/Viewer.tsx index a7e59a66..3fac3f09 100644 --- a/docs/components/DynamicImports/Viewer.tsx +++ b/docs/components/DynamicImports/Viewer.tsx @@ -1,6 +1,7 @@ import { type CustomDisplay, ViewerConfigOptions, + Plugin, } from "src/context/viewer-context"; import dynamic from "next/dynamic"; import { isDark } from "docs/lib/theme"; @@ -20,11 +21,13 @@ const CloverViewer = ({ options, customDisplays, iiifContentSearchQuery, + plugins, }: { iiifContent: string; options?: ViewerConfigOptions; customDisplays?: Array; iiifContentSearchQuery?: ContentSearchQuery; + plugins?: Array; }) => { const router = useRouter(); const iiifResource = router.query["iiif-content"] @@ -40,6 +43,7 @@ const CloverViewer = ({ options={{ ...options, background }} key={iiifResource} {...(customDisplays && { customDisplays })} + {...(plugins && { plugins })} /> ); }; diff --git a/src/components/Viewer/Player/Player.test.tsx b/src/components/Viewer/Player/Player.test.tsx index 2e6a4629..dec5f771 100644 --- a/src/components/Viewer/Player/Player.test.tsx +++ b/src/components/Viewer/Player/Player.test.tsx @@ -67,6 +67,7 @@ describe("Player component", () => { collection: {}, configOptions: {}, customDisplays: [], + plugins: [], isInformationOpen: false, isLoaded: false, vault, @@ -186,6 +187,7 @@ describe("Player component", () => { collection: {}, configOptions: {}, customDisplays: [], + plugins: [], isInformationOpen: false, isLoaded: false, vault, diff --git a/src/components/Viewer/Viewer/Viewer.tsx b/src/components/Viewer/Viewer/Viewer.tsx index f2143da0..b26809d1 100644 --- a/src/components/Viewer/Viewer/Viewer.tsx +++ b/src/components/Viewer/Viewer/Viewer.tsx @@ -57,6 +57,7 @@ const Viewer: React.FC = ({ contentSearchVault, configOptions, openSeadragonViewer, + plugins, } = viewerState; const absoluteCanvasHeights = ["100%", "auto"]; @@ -186,6 +187,20 @@ const Viewer: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [openSeadragonViewer, contentSearchResource]); + function renderPlugins(activeCanvas, openSeadragonViewer) { + return plugins.map((plugin, i) => { + const Plugin = plugin.component as unknown as React.ElementType; + return ( + + ); + }); + } + return ( = ({ manifestLabel={manifest.label as InternationalString} manifestId={manifest.id} /> + {activeCanvas && + openSeadragonViewer && + renderPlugins(activeCanvas, openSeadragonViewer)} void; customDisplays?: Array; + plugins?: Array; customTheme?: any; iiifContent: string; id?: string; @@ -35,6 +37,7 @@ export interface CloverViewerProps { const CloverViewer: React.FC = ({ canvasIdCallback = () => {}, customDisplays = [], + plugins = [], customTheme, iiifContent, id, @@ -61,6 +64,7 @@ const CloverViewer: React.FC = ({ initialState={{ ...defaultState, customDisplays, + plugins, isAutoScrollEnabled: autoScrollOptions.enabled, isInformationOpen: Boolean(options?.informationPanel?.open), vault: new Vault({ diff --git a/src/context/viewer-context.tsx b/src/context/viewer-context.tsx index 92fde0ae..dc83716c 100644 --- a/src/context/viewer-context.tsx +++ b/src/context/viewer-context.tsx @@ -138,6 +138,10 @@ export type CustomDisplay = { paintingFormat?: string[]; }; }; +export type Plugin = { + component: React.ElementType; + componentProps?: Record; +}; export interface ViewerContextStore { activeCanvas: string; @@ -146,6 +150,7 @@ export interface ViewerContextStore { collection?: CollectionNormalized | Record; configOptions: ViewerConfigOptions; customDisplays: Array; + plugins: Array; isAutoScrollEnabled?: boolean; isAutoScrolling?: boolean; isInformationOpen: boolean; @@ -210,6 +215,7 @@ export const defaultState: ViewerContextStore = { collection: {}, configOptions: defaultConfigOptions, customDisplays: [], + plugins: [], isAutoScrollEnabled: expandedAutoScrollOptions.enabled, isAutoScrolling: false, isInformationOpen: defaultConfigOptions?.informationPanel?.open, From eb03da7948f49b133ef6f6fdccc571dace8c75ef Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Sat, 24 Feb 2024 01:17:22 -0800 Subject: [PATCH 03/30] refactor getAnnotationResources to handle external annotation pages --- src/components/Viewer/Viewer/Viewer.tsx | 24 ++++---- .../use-iiif/get-annotation-resources.ts | 52 +++++++++++++++++ .../use-iiif/getAnnotationResources.test.ts | 56 ++++++++++++++++--- src/hooks/use-iiif/getAnnotationResources.ts | 41 +++++++++----- 4 files changed, 139 insertions(+), 34 deletions(-) diff --git a/src/components/Viewer/Viewer/Viewer.tsx b/src/components/Viewer/Viewer/Viewer.tsx index b26809d1..01abca84 100644 --- a/src/components/Viewer/Viewer/Viewer.tsx +++ b/src/components/Viewer/Viewer/Viewer.tsx @@ -120,19 +120,17 @@ const Viewer: React.FC = ({ ); setPainting(painting); } - - const resources = getAnnotationResources(vault, activeCanvas); - - if (resources.length > 0) { - viewerDispatch({ - type: "updateInformationOpen", - isInformationOpen: true, - }); - } - - setAnnotationResources(resources); - setIsInformationPanel(resources.length !== 0); - }, [activeCanvas, vault, viewerDispatch]); + getAnnotationResources(vault, activeCanvas).then((resources) => { + if (resources.length > 0) { + viewerDispatch({ + type: "updateInformationOpen", + isInformationOpen: true, + }); + } + setAnnotationResources(resources); + setIsInformationPanel(resources.length !== 0); + }); + }, [activeCanvas, annotationResources.length, vault, viewerDispatch]); const hasSearchService = manifest.service.some( (service: any) => service.type === "SearchService2", diff --git a/src/fixtures/use-iiif/get-annotation-resources.ts b/src/fixtures/use-iiif/get-annotation-resources.ts index eda6f82a..14580989 100644 --- a/src/fixtures/use-iiif/get-annotation-resources.ts +++ b/src/fixtures/use-iiif/get-annotation-resources.ts @@ -559,3 +559,55 @@ export const recipe0219captionFile = { }, ], }; + +export const referencedAnnotations = { + "@context": "http://iiif.io/api/presentation/3/context.json", + id: "https://iiif.io/api/cookbook/recipe/0269-embedded-or-referenced-annotations/manifest.json", + type: "Manifest", + label: { + en: ["Picture of Göttingen taken during the 2019 IIIF Conference"], + }, + items: [ + { + id: "https://iiif.io/api/cookbook/recipe/0269-embedded-or-referenced-annotations/canvas-1", + type: "Canvas", + height: 3024, + width: 4032, + items: [ + { + id: "https://iiif.io/api/cookbook/recipe/0269-embedded-or-referenced-annotations/canvas-1/annopage-1", + type: "AnnotationPage", + items: [ + { + id: "https://iiif.io/api/cookbook/recipe/0269-embedded-or-referenced-annotations/canvas-1/annopage-1/anno-1", + type: "Annotation", + motivation: "painting", + body: { + id: "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", + type: "Image", + format: "image/jpeg", + height: 3024, + width: 4032, + service: [ + { + id: "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", + profile: "level1", + type: "ImageService3", + }, + ], + }, + target: + "https://iiif.io/api/cookbook/recipe/0269-embedded-or-referenced-annotations/canvas-1", + }, + ], + }, + ], + annotations: [ + { + id: "https://iiif.io/api/cookbook/recipe/0269-embedded-or-referenced-annotations/annotationpage.json", + type: "AnnotationPage", + }, + ], + }, + ], +}; diff --git a/src/hooks/use-iiif/getAnnotationResources.test.ts b/src/hooks/use-iiif/getAnnotationResources.test.ts index 30db1e1e..d4d8c397 100644 --- a/src/hooks/use-iiif/getAnnotationResources.test.ts +++ b/src/hooks/use-iiif/getAnnotationResources.test.ts @@ -5,6 +5,7 @@ import { recipe0219captionFile, simpleAnnotations, simpleTagging, + referencedAnnotations, } from "src/fixtures/use-iiif/get-annotation-resources"; import { Vault } from "@iiif/vault"; @@ -19,7 +20,7 @@ describe("getAnnotationResources method", () => { const vault = new Vault(); await vault.loadManifest("", simpleAnnotations); - const result = getAnnotationResources( + const result = await getAnnotationResources( vault, "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1", ); @@ -60,7 +61,7 @@ describe("getAnnotationResources method", () => { const vault = new Vault(); await vault.loadManifest("", simpleTagging); - const result = getAnnotationResources( + const result = await getAnnotationResources( vault, "https://iiif.io/api/cookbook/recipe/0021-tagging/canvas/p1", ); @@ -100,7 +101,7 @@ describe("getAnnotationResources method", () => { const vault = new Vault(); await vault.loadManifest("", nonRectangularPolygon); - const result = getAnnotationResources( + const result = await getAnnotationResources( vault, "https://iiif.io/api/cookbook/recipe/0261-non-rectangular-commenting/canvas/p1", ); @@ -140,7 +141,7 @@ describe("getAnnotationResources method", () => { const vault = new Vault(); await vault.loadManifest("", multipleHighlighting); - const result = getAnnotationResources( + const result = await getAnnotationResources( vault, "http://localhost:3000/manifest/newspaper/canvas/i1p1", ); @@ -192,7 +193,7 @@ describe("getAnnotationResources method", () => { const vault = new Vault(); await vault.loadManifest("", imagesAnnotations); - const result = getAnnotationResources( + const result = await getAnnotationResources( vault, "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1", ); @@ -232,7 +233,7 @@ describe("getAnnotationResources method", () => { const vault = new Vault(); await vault.loadManifest("", manifestNoAnnotations); - const result = getAnnotationResources( + const result = await getAnnotationResources( vault, "https://api.dc.library.northwestern.edu/api/v2/works/57446da0-dc8b-4be6-998d-efb67c71f654?as=iiif/canvas/access/0", ); @@ -273,13 +274,54 @@ describe("getAnnotationResources method", () => { }, ]; - const result = getAnnotationResources( + const result = await getAnnotationResources( vault, "https://iiif.io/api/cookbook/recipe/0219-using-caption-file/canvas", ); expect(result).toStrictEqual(expected); }); + + it("processes manifests with annotations stored on separate document", async () => { + const vault = new Vault(); + await vault.loadManifest("", referencedAnnotations); + + const result = await getAnnotationResources( + vault, + referencedAnnotations.items[0].id, + ); + + const expected = [ + { + "@context": "http://iiif.io/api/presentation/3/context.json", + behavior: [], + homepage: [], + id: "https://iiif.io/api/cookbook/recipe/0269-embedded-or-referenced-annotations/annotationpage.json", + items: [ + { + id: "https://iiif.io/api/cookbook/recipe/0269-embedded-or-referenced-annotations/canvas-1/annopage-2/anno-1", + type: "Annotation", + }, + ], + label: { + none: ["Annotations"], + }, + logo: [], + metadata: [], + motivation: null, + provider: [], + rendering: [], + requiredStatement: null, + rights: null, + seeAlso: [], + service: [], + summary: null, + thumbnail: [], + type: "AnnotationPage", + }, + ]; + expect(result).toStrictEqual(expected); + }); }); describe("getContentSearchResources", () => { diff --git a/src/hooks/use-iiif/getAnnotationResources.ts b/src/hooks/use-iiif/getAnnotationResources.ts index 9ca1a20e..546773ab 100644 --- a/src/hooks/use-iiif/getAnnotationResources.ts +++ b/src/hooks/use-iiif/getAnnotationResources.ts @@ -5,10 +5,10 @@ import { } from "src/types/annotations"; import { CanvasNormalized } from "@iiif/presentation-3"; -export const getAnnotationResources = ( +export const getAnnotationResources = async ( vault: any, activeCanvas: string, -): AnnotationResources => { +): Promise => { const canvas: CanvasNormalized = vault.get({ id: activeCanvas, type: "Canvas", @@ -21,19 +21,32 @@ export const getAnnotationResources = ( /** * Filter out annotation pages that don't have any Annotations in the items array. */ - return annotationPages - .filter((annotationPage) => { - if (!annotationPage.items || !annotationPage.items.length) return false; - return annotationPage; - }) - .map((annotationPage) => { - /** - * If the annotation page doesn't have a label, add a default label. - * Set this value in a CONFIG and not here. - */ + const filteredPages = annotationPages.filter((annotationPage) => { + if (!annotationPage.items) return false; + return annotationPage; + }); + + const pages: AnnotationResources = []; + for (const annotationPage of filteredPages) { + // handle embedded annotations + if (annotationPage.items.length > 0) { const label = annotationPage.label || { none: ["Annotations"] }; - return { ...annotationPage, label }; - }); + pages.push({ ...annotationPage, label: label }); + // handle referenced annotations that are in a separate AnnotationPage + } else { + const annotationPageReferenced = await vault.load(annotationPage.id); + if ( + annotationPageReferenced.items && + annotationPageReferenced.items.length > 0 + ) { + const label = annotationPageReferenced.label || { + none: ["Annotations"], + }; + pages.push({ ...annotationPageReferenced, label: label }); + } + } + } + return pages; }; export const getContentSearchResources = async ( From 98fdeaeb0750cb1cc98107f008c148a89c874159 Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Sat, 24 Feb 2024 08:29:59 -0800 Subject: [PATCH 04/30] refactor parseAnnotationTarget to handle FragmentSelector --- src/lib/annotation-helpers.test.ts | 124 +++++++++++++++++++++++++++++ src/lib/annotation-helpers.ts | 25 +++++- 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/lib/annotation-helpers.test.ts diff --git a/src/lib/annotation-helpers.test.ts b/src/lib/annotation-helpers.test.ts new file mode 100644 index 00000000..0afc923d --- /dev/null +++ b/src/lib/annotation-helpers.test.ts @@ -0,0 +1,124 @@ +import { + parseAnnotationTarget, + AnnotationTargetExtended, +} from "./annotation-helpers"; + +describe("parseAnnotationTarget", () => { + it("handles target strings with xywh", () => { + const target = "http://example.com/canvas/1#xywh=100,200,300,400"; + + const result = parseAnnotationTarget(target); + + const expected = { + id: "http://example.com/canvas/1", + rect: { + x: 100, + y: 200, + w: 300, + h: 400, + }, + }; + expect(result).toEqual(expected); + }); + + it("handles target strings with t", () => { + const target = "http://example.com/canvas/1#t=100"; + + const result = parseAnnotationTarget(target); + + const expected = { + id: "http://example.com/canvas/1", + t: "100", + }; + expect(result).toEqual(expected); + }); + + it("handles target objects with PointSelector", () => { + const target: AnnotationTargetExtended = { + type: "SpecificResource", + source: "http://example.com/canvas/1", + selector: { + type: "PointSelector", + x: 100, + y: 200, + }, + }; + + const result = parseAnnotationTarget(target); + + const expected = { + id: "http://example.com/canvas/1", + point: { + x: 100, + y: 200, + }, + }; + expect(result).toEqual(expected); + }); + + it("handles target objects with SvgSelector", () => { + const target: AnnotationTargetExtended = { + type: "SpecificResource", + source: "http://example.com/canvas/1", + selector: { + type: "SvgSelector", + value: + '', + }, + }; + + const result = parseAnnotationTarget(target); + + const expected = { + id: "http://example.com/canvas/1", + svg: '', + }; + expect(result).toEqual(expected); + }); + + it("handles target objects with FragmentSelector and xywh", () => { + const target: AnnotationTargetExtended = { + type: "SpecificResource", + source: { + id: "http://example.com/canvas/1", + type: "Canvas", + partOf: [ + { + id: "http://example.com/manifest.json", + type: "Manifest", + }, + ], + }, + selector: { + conformsTo: "http://www.w3.org/TR/media-frags/", + type: "FragmentSelector", + value: "xywh=100,200,300,400", + }, + }; + + const result = parseAnnotationTarget(target); + + const expected = { + id: "http://example.com/canvas/1", + rect: { + x: 100, + y: 200, + w: 300, + h: 400, + }, + }; + expect(result).toEqual(expected); + }); +}); + +/* +{ +selector :{ +conformsTo:"http://www.w3.org/TR/media-frags/", +type:"FragmentSelector", +value:"xywh=86.18675994873047,1189.02587890625,910.7970657348633,653.7779541015625" +} +source:"https://iiif.io/api/image/3.0/example/reference/4ce82cef49fb16798f4c2440307c3d6f-newspaper-p1" +type:"SpecificResource" +} +*/ diff --git a/src/lib/annotation-helpers.ts b/src/lib/annotation-helpers.ts index 789bea7c..2b16eae1 100644 --- a/src/lib/annotation-helpers.ts +++ b/src/lib/annotation-helpers.ts @@ -1,7 +1,7 @@ import { AnnotationTarget } from "@iiif/presentation-3"; import { ParsedAnnotationTarget } from "src/types/annotations"; -type AnnotationTargetExtended = AnnotationTarget & { +export type AnnotationTargetExtended = AnnotationTarget & { selector?: any; source?: string; svg?: string; @@ -50,6 +50,29 @@ const parseAnnotationTarget = (target: AnnotationTargetExtended | string) => { id: target.source, svg: target.selector.value, }; + } else if (target.selector?.type === "FragmentSelector") { + if ( + target.selector?.value.includes("xywh=") && + target.source.type == "Canvas" && + target.source.id + ) { + const parts = target.selector?.value.split("xywh="); + if (parts && parts[1]) { + const [x, y, w, h] = parts[1] + .split(",") + .map((value) => Number(value)); + + parsedTarget = { + id: target.source.id, + rect: { + x, + y, + w, + h, + }, + }; + } + } } } From a4a3f9628d81a340537d5b8c50a09b9173206d6f Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Tue, 27 Feb 2024 08:55:16 -0800 Subject: [PATCH 05/30] add ignoreAnnotationOverlaysLabels to not add overlays to annotations --- src/context/viewer-context.tsx | 2 + .../use-iiif/get-annotation-resources.ts | 92 ++++++++++++ src/lib/annotation-helpers.test.ts | 136 ++++++++++++++++++ src/lib/annotation-helpers.ts | 37 ++++- 4 files changed, 265 insertions(+), 2 deletions(-) diff --git a/src/context/viewer-context.tsx b/src/context/viewer-context.tsx index dc83716c..a753f90f 100644 --- a/src/context/viewer-context.tsx +++ b/src/context/viewer-context.tsx @@ -27,6 +27,7 @@ export type ViewerConfigOptions = { overlays?: OverlayOptions; }; ignoreCaptionLabels?: string[]; + ignoreAnnotationOverlaysLabels?: string[]; informationPanel?: { open?: boolean; renderAbout?: boolean; @@ -97,6 +98,7 @@ const defaultConfigOptions = { }, }, ignoreCaptionLabels: [], + ignoreAnnotationOverlaysLabels: [], informationPanel: { vtt: { autoScroll: { diff --git a/src/fixtures/use-iiif/get-annotation-resources.ts b/src/fixtures/use-iiif/get-annotation-resources.ts index 14580989..5f949f3c 100644 --- a/src/fixtures/use-iiif/get-annotation-resources.ts +++ b/src/fixtures/use-iiif/get-annotation-resources.ts @@ -611,3 +611,95 @@ export const referencedAnnotations = { }, ], }; + +export const multiplePages = { + "@context": ["http://iiif.io/api/presentation/3/context.json"], + id: "http://localhost:3000/manifest/newspaper/newspaper_issue_1.json", + type: "Manifest", + label: { + de: ["1. Berliner Tageblatt - 1925-02-16"], + }, + items: [ + { + id: "http://localhost:3000/manifest/newspaper/canvas/i1p1", + type: "Canvas", + height: 5000, + width: 3602, + label: { + none: ["p. 1"], + }, + items: [ + { + id: "http://localhost:3000/manifest/newspaper/annotation_page_painting/ap1", + type: "AnnotationPage", + items: [ + { + id: "http://localhost:3000/manifest/newspaper/annotation/p1", + type: "Annotation", + motivation: "painting", + body: { + id: "https://iiif.io/api/image/3.0/example/reference/4ce82cef49fb16798f4c2440307c3d6f-newspaper-p1/full/max/0/default.jpg", + type: "Image", + format: "image/jpeg", + service: [ + { + id: "https://iiif.io/api/image/3.0/example/reference/4ce82cef49fb16798f4c2440307c3d6f-newspaper-p1", + type: "ImageService3", + profile: "level1", + }, + ], + }, + target: "http://localhost:3000/manifest/newspaper/canvas/i1p1", + }, + ], + }, + ], + annotations: [ + { + id: "http://localhost:3000/manifest/newspaper/newspaper_issue_1-anno_p10.json", + type: "AnnotationPage", + label: { + en: ["Search results"], + }, + items: [ + { + id: "http://localhost:3000/manifest/newspaper/newspaper_issue_1-anno_p1.json-1", + type: "Annotation", + motivation: "highlighting", + body: { + type: "TextualBody", + format: "text/plain", + language: "de", + value: "Berliner", + }, + target: + "http://localhost:3000/manifest/newspaper/canvas/i1p1#xywh=839,3259,118,27", + }, + ], + }, + { + id: "http://localhost:3000/manifest/newspaper/newspaper_issue_1-anno_p2.json", + type: "AnnotationPage", + label: { + en: ["Clippings"], + }, + items: [ + { + id: "http://localhost:3000/manifest/newspaper/newspaper_issue_1-anno_p2.json-2", + type: "Annotation", + motivation: "commenting", + body: { + type: "TextualBody", + format: "text/plain", + language: "de", + value: "Berliner", + }, + target: + "http://localhost:3000/manifest/newspaper/canvas/i1p2#xywh=161,459,1063,329", + }, + ], + }, + ], + }, + ], +}; diff --git a/src/lib/annotation-helpers.test.ts b/src/lib/annotation-helpers.test.ts index 0afc923d..1cb50fce 100644 --- a/src/lib/annotation-helpers.test.ts +++ b/src/lib/annotation-helpers.test.ts @@ -1,7 +1,15 @@ import { parseAnnotationTarget, AnnotationTargetExtended, + parseAnnotationsFromAnnotationResources, } from "./annotation-helpers"; +import { Vault } from "@iiif/vault"; +import { + simpleTagging, + multiplePages, +} from "src/fixtures/use-iiif/get-annotation-resources"; + +import { getAnnotationResources } from "src/hooks/use-iiif/getAnnotationResources"; describe("parseAnnotationTarget", () => { it("handles target strings with xywh", () => { @@ -111,6 +119,134 @@ describe("parseAnnotationTarget", () => { }); }); +describe("parseAnnotationsFromAnnotationResources", () => { + it("returns annotations from annotation resource", async () => { + const config = {}; + const vault = new Vault(); + await vault.loadManifest("", structuredClone(simpleTagging)); + const annotationResources = await getAnnotationResources( + vault, + simpleTagging.items[0].id, + ); + + const res = parseAnnotationsFromAnnotationResources( + annotationResources, + vault, + config, + ); + + const expected = [ + { + body: [ + { + id: "vault://605b9d93", + type: "ContentResource", + }, + ], + id: "https://iiif.io/api/cookbook/recipe/0021-tagging/annotation/p0002-tag", + motivation: ["tagging"], + target: + "https://iiif.io/api/cookbook/recipe/0021-tagging/canvas/p1#xywh=265,661,1260,1239", + type: "Annotation", + }, + ]; + expect(res).toStrictEqual(expected); + }); + + it("returns annotations if annotation resource has multiple annotation pages", async () => { + const config = {}; + const vault = new Vault(); + await vault.loadManifest("", structuredClone(multiplePages)); + const annotationResources = await getAnnotationResources( + vault, + multiplePages.items[0].id, + ); + + const res = parseAnnotationsFromAnnotationResources( + annotationResources, + vault, + config, + ); + + const expected = [ + { + body: [ + { + id: "vault://772e4338", + type: "ContentResource", + }, + ], + id: "http://localhost:3000/manifest/newspaper/newspaper_issue_1-anno_p1.json-1", + motivation: ["highlighting"], + target: + "http://localhost:3000/manifest/newspaper/canvas/i1p1#xywh=839,3259,118,27", + type: "Annotation", + }, + { + body: [ + { + id: "vault://772e4338", + type: "ContentResource", + }, + ], + id: "http://localhost:3000/manifest/newspaper/newspaper_issue_1-anno_p2.json-2", + motivation: ["commenting"], + target: + "http://localhost:3000/manifest/newspaper/canvas/i1p2#xywh=161,459,1063,329", + type: "Annotation", + }, + ]; + expect(res).toStrictEqual(expected); + }); + + it("ignores annotations in ignoreAnnotationOverlaysLabels", async () => { + const config = { ignoreAnnotationOverlaysLabels: ["Clippings"] }; + const vault = new Vault(); + await vault.loadManifest("", structuredClone(multiplePages)); + const annotationResources = await getAnnotationResources( + vault, + multiplePages.items[0].id, + ); + + const res = parseAnnotationsFromAnnotationResources( + annotationResources, + vault, + config, + ); + + const expected = [ + { + body: [ + { + id: "vault://772e4338", + type: "ContentResource", + }, + ], + id: "http://localhost:3000/manifest/newspaper/newspaper_issue_1-anno_p1.json-1", + motivation: ["highlighting"], + target: + "http://localhost:3000/manifest/newspaper/canvas/i1p1#xywh=839,3259,118,27", + type: "Annotation", + }, + ]; + expect(res).toStrictEqual(expected); + }); + + it("returns empty array if annotation resource is empty array", () => { + const config = {}; + const annotationResources = []; + const vault = new Vault(); + + const res = parseAnnotationsFromAnnotationResources( + annotationResources, + vault, + config, + ); + + expect(res).toStrictEqual([]); + }); +}); + /* { selector :{ diff --git a/src/lib/annotation-helpers.ts b/src/lib/annotation-helpers.ts index 2b16eae1..e111925a 100644 --- a/src/lib/annotation-helpers.ts +++ b/src/lib/annotation-helpers.ts @@ -1,5 +1,8 @@ -import { AnnotationTarget } from "@iiif/presentation-3"; +import { AnnotationTarget, AnnotationNormalized } from "@iiif/presentation-3"; import { ParsedAnnotationTarget } from "src/types/annotations"; +import { getLabel } from "src/hooks/use-iiif"; +import { AnnotationResources } from "src/types/annotations"; +import { type ViewerConfigOptions } from "src/context/viewer-context"; export type AnnotationTargetExtended = AnnotationTarget & { selector?: any; @@ -79,4 +82,34 @@ const parseAnnotationTarget = (target: AnnotationTargetExtended | string) => { return parsedTarget; }; -export { parseAnnotationTarget }; +const parseAnnotationsFromAnnotationResources = ( + annotationResources: AnnotationResources, + vault: any, + configOptions: ViewerConfigOptions, +) => { + const annotations: Array = []; + annotationResources + .filter((annotationResource) => { + if (annotationResource.label) { + const label = getLabel(annotationResource.label); + if (Array.isArray(label)) { + return !label.some( + (value) => + configOptions.ignoreAnnotationOverlaysLabels?.includes(value), + ); + } + } + + return true; + }) + .forEach((annotationResource) => { + annotationResource?.items?.forEach((item) => { + const annotation = vault.get(item.id); + annotations.push(annotation as unknown as AnnotationNormalized); + }); + }); + + return annotations; +}; + +export { parseAnnotationTarget, parseAnnotationsFromAnnotationResources }; From 2836f7149cd4f8d280760fcbdd66512c9464ce05 Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Mon, 4 Mar 2024 20:18:33 -0800 Subject: [PATCH 06/30] add infomation panel to plugins --- .../InformationPanel/Annotation/Page.tsx | 42 ++++++++++++++++- .../InformationPanel/InformationPanel.tsx | 46 ++++++++++++++++++- src/context/viewer-context.tsx | 12 ++++- 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/components/Viewer/InformationPanel/Annotation/Page.tsx b/src/components/Viewer/InformationPanel/Annotation/Page.tsx index b9bccd56..93073ce7 100644 --- a/src/components/Viewer/InformationPanel/Annotation/Page.tsx +++ b/src/components/Viewer/InformationPanel/Annotation/Page.tsx @@ -1,6 +1,7 @@ import { AnnotationNormalized, AnnotationPageNormalized, + CanvasNormalized, } from "@iiif/presentation-3"; import { ViewerContextStore, useViewerState } from "src/context/viewer-context"; @@ -13,7 +14,8 @@ type Props = { }; export const AnnotationPage: React.FC = ({ annotationPage }) => { const viewerState: ViewerContextStore = useViewerState(); - const { vault } = viewerState; + const { vault, openSeadragonViewer, activeCanvas, configOptions, plugins } = + viewerState; if ( !annotationPage || @@ -28,6 +30,44 @@ export const AnnotationPage: React.FC = ({ annotationPage }) => { if (!annotations) return <>; + const plugin = plugins.find((plugin) => { + let match = false; + if (plugin.informationPanel?.annotationPageId) { + match = plugin.informationPanel.annotationPageId.includes( + annotationPage.id, + ); + } + + return match; + }); + + const PluginInformationPanel = plugin?.informationPanel + ?.component as unknown as React.ElementType; + + if (PluginInformationPanel) { + const annotationsWithBodies = annotations.map((annotation) => { + return { + ...annotation, + body: annotation.body.map((body) => vault.get(body.id)), + }; + }); + const canvas: CanvasNormalized = vault.get({ + id: activeCanvas, + type: "Canvas", + }); + + return ( + + + + ); + } + return ( {annotations?.map((annotation) => ( diff --git a/src/components/Viewer/InformationPanel/InformationPanel.tsx b/src/components/Viewer/InformationPanel/InformationPanel.tsx index b3939348..6623fbca 100644 --- a/src/components/Viewer/InformationPanel/InformationPanel.tsx +++ b/src/components/Viewer/InformationPanel/InformationPanel.tsx @@ -46,10 +46,13 @@ export const InformationPanel: React.FC = ({ const viewerState: ViewerContextStore = useViewerState(); const { isAutoScrolling, - configOptions: { informationPanel }, isUserScrolling, vault, + openSeadragonViewer, + configOptions, + plugins, } = viewerState; + const { informationPanel } = configOptions; const canvas: CanvasNormalized = vault.get({ id: activeCanvas, @@ -62,6 +65,28 @@ export const InformationPanel: React.FC = ({ const renderAnnotation = informationPanel?.renderAnnotation; const renderContentSearch = informationPanel?.renderContentSearch; + const pluginsWithoutAnnotations = plugins.filter((plugin) => { + let match = false; + if (plugin.informationPanel?.annotationPageId === undefined) { + match = true; + } + + return match; + }); + + function renderPluginInformationPanel(plugin) { + const PluginInformationPanel = plugin?.informationPanel + ?.component as unknown as React.ElementType; + + return ( + + ); + } + useEffect(() => { if (activeResource) { return; @@ -140,6 +165,18 @@ export const InformationPanel: React.FC = ({ ); diff --git a/src/context/viewer-context.tsx b/src/context/viewer-context.tsx index a753f90f..182f59b4 100644 --- a/src/context/viewer-context.tsx +++ b/src/context/viewer-context.tsx @@ -1,7 +1,10 @@ import OpenSeadragon, { Options as OpenSeadragonOptions } from "openseadragon"; import React, { useReducer } from "react"; -import { CollectionNormalized } from "@iiif/presentation-3"; +import { + CollectionNormalized, + InternationalString, +} from "@iiif/presentation-3"; import { IncomingHttpHeaders } from "http"; import { Vault } from "@iiif/vault"; import { deepMerge } from "src/lib/utils"; @@ -141,8 +144,15 @@ export type CustomDisplay = { }; }; export type Plugin = { + id: string; component: React.ElementType; componentProps?: Record; + informationPanel?: { + component: React.ElementType; + componentProps?: Record; + annotationPageId?: string[]; + label?: InternationalString; + }; }; export interface ViewerContextStore { From 0528f09b0593e1ae347c39cc7b5baca42c69728a Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Tue, 5 Mar 2024 15:45:51 -0800 Subject: [PATCH 07/30] export annotation and helpers --- src/index.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index 8fc6ff73..c3be2aff 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,23 @@ import Primitives from "src/components/Primitives"; import Scroll from "src/components/Scroll"; import Slider from "src/components/Slider"; import Viewer from "src/components/Viewer"; +import { + parseAnnotationTarget, + parseAnnotationsFromAnnotationResources, + type AnnotationTargetExtended, +} from "src/lib/annotation-helpers"; +import { createOpenSeadragonRect } from "src/lib/openseadragon-helpers"; -export { Image, Primitives, Scroll, Slider, Viewer }; +export { + Image, + Primitives, + Scroll, + Slider, + Viewer, + parseAnnotationTarget, + parseAnnotationsFromAnnotationResources, + type AnnotationTargetExtended, + createOpenSeadragonRect, +}; export default Viewer; From 366e081de670dd0ba47f7195e186ac20d0ae6a8a Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Tue, 5 Mar 2024 11:19:39 -0800 Subject: [PATCH 08/30] fix - add try catch to getAnnotationResources --- src/hooks/use-iiif/getAnnotationResources.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hooks/use-iiif/getAnnotationResources.ts b/src/hooks/use-iiif/getAnnotationResources.ts index 546773ab..3406db66 100644 --- a/src/hooks/use-iiif/getAnnotationResources.ts +++ b/src/hooks/use-iiif/getAnnotationResources.ts @@ -34,7 +34,12 @@ export const getAnnotationResources = async ( pages.push({ ...annotationPage, label: label }); // handle referenced annotations that are in a separate AnnotationPage } else { - const annotationPageReferenced = await vault.load(annotationPage.id); + let annotationPageReferenced = {} as any; + try { + annotationPageReferenced = await vault.load(annotationPage.id); + } catch (error) { + console.log(error); + } if ( annotationPageReferenced.items && annotationPageReferenced.items.length > 0 From 451dd0543bcd6c54c1cafb19944a3c794ed02f23 Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Wed, 6 Mar 2024 17:52:46 -0800 Subject: [PATCH 09/30] refactor plugin component and info panel to have same props --- .../InformationPanel/Annotation/Page.tsx | 47 ++++++++++++------- .../InformationPanel/InformationPanel.tsx | 15 ++++-- src/components/Viewer/Viewer/Viewer.tsx | 25 ++++++---- src/context/viewer-context.tsx | 1 - 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/components/Viewer/InformationPanel/Annotation/Page.tsx b/src/components/Viewer/InformationPanel/Annotation/Page.tsx index 93073ce7..e392a4b1 100644 --- a/src/components/Viewer/InformationPanel/Annotation/Page.tsx +++ b/src/components/Viewer/InformationPanel/Annotation/Page.tsx @@ -3,7 +3,11 @@ import { AnnotationPageNormalized, CanvasNormalized, } from "@iiif/presentation-3"; -import { ViewerContextStore, useViewerState } from "src/context/viewer-context"; +import { + ViewerContextStore, + useViewerState, + useViewerDispatch, +} from "src/context/viewer-context"; import AnnotationItem from "src/components/Viewer/InformationPanel/Annotation/Item"; import { Group } from "src/components/Viewer/InformationPanel/Annotation/Item.styled"; @@ -14,8 +18,14 @@ type Props = { }; export const AnnotationPage: React.FC = ({ annotationPage }) => { const viewerState: ViewerContextStore = useViewerState(); - const { vault, openSeadragonViewer, activeCanvas, configOptions, plugins } = - viewerState; + const { + vault, + openSeadragonViewer, + activeCanvas, + configOptions, + plugins, + activeManifest, + } = viewerState; if ( !annotationPage || @@ -30,39 +40,44 @@ export const AnnotationPage: React.FC = ({ annotationPage }) => { if (!annotations) return <>; + const canvas: CanvasNormalized = vault.get({ + id: activeCanvas, + type: "Canvas", + }); + const plugin = plugins.find((plugin) => { let match = false; - if (plugin.informationPanel?.annotationPageId) { - match = plugin.informationPanel.annotationPageId.includes( - annotationPage.id, - ); + const annotationServer = + plugin.informationPanel?.componentProps?.annotationServer; + if (annotationServer) { + match = annotationServer === annotationPage.id; } return match; }); - const PluginInformationPanel = plugin?.informationPanel + const PluginInformationPanelComponent = plugin?.informationPanel ?.component as unknown as React.ElementType; - if (PluginInformationPanel) { + if (PluginInformationPanelComponent) { const annotationsWithBodies = annotations.map((annotation) => { return { ...annotation, body: annotation.body.map((body) => vault.get(body.id)), }; }); - const canvas: CanvasNormalized = vault.get({ - id: activeCanvas, - type: "Canvas", - }); return ( - ); diff --git a/src/components/Viewer/InformationPanel/InformationPanel.tsx b/src/components/Viewer/InformationPanel/InformationPanel.tsx index 6623fbca..dd335f03 100644 --- a/src/components/Viewer/InformationPanel/InformationPanel.tsx +++ b/src/components/Viewer/InformationPanel/InformationPanel.tsx @@ -51,6 +51,7 @@ export const InformationPanel: React.FC = ({ openSeadragonViewer, configOptions, plugins, + activeManifest, } = viewerState; const { informationPanel } = configOptions; @@ -67,7 +68,9 @@ export const InformationPanel: React.FC = ({ const pluginsWithoutAnnotations = plugins.filter((plugin) => { let match = false; - if (plugin.informationPanel?.annotationPageId === undefined) { + const annotationServer = + plugin.informationPanel?.componentProps?.annotationServer; + if (annotationServer === undefined) { match = true; } @@ -75,14 +78,18 @@ export const InformationPanel: React.FC = ({ }); function renderPluginInformationPanel(plugin) { - const PluginInformationPanel = plugin?.informationPanel + const PluginInformationPanelComponent = plugin?.informationPanel ?.component as unknown as React.ElementType; return ( - ); } diff --git a/src/components/Viewer/Viewer/Viewer.tsx b/src/components/Viewer/Viewer/Viewer.tsx index 01abca84..b6842528 100644 --- a/src/components/Viewer/Viewer/Viewer.tsx +++ b/src/components/Viewer/Viewer/Viewer.tsx @@ -90,6 +90,11 @@ const Viewer: React.FC = ({ [viewerDispatch], ); + const canvas: CanvasNormalized = vault.get({ + id: activeCanvas, + type: "Canvas", + }); + useEffect(() => { if (configOptions?.informationPanel?.open) { setInformationOpen(!isSmallViewport); @@ -185,16 +190,20 @@ const Viewer: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [openSeadragonViewer, contentSearchResource]); - function renderPlugins(activeCanvas, openSeadragonViewer) { + function renderPlugins() { return plugins.map((plugin, i) => { - const Plugin = plugin.component as unknown as React.ElementType; + const PluginComponent = plugin.component as unknown as React.ElementType; return ( - + useViewerDispatch={useViewerDispatch} + useViewerState={useViewerState} + > ); }); } @@ -217,9 +226,7 @@ const Viewer: React.FC = ({ manifestLabel={manifest.label as InternationalString} manifestId={manifest.id} /> - {activeCanvas && - openSeadragonViewer && - renderPlugins(activeCanvas, openSeadragonViewer)} + {activeCanvas && openSeadragonViewer && renderPlugins()} ; - annotationPageId?: string[]; label?: InternationalString; }; }; From 10885345e3064ac0ca7e4291898cb93ac322064e Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Wed, 6 Mar 2024 17:53:42 -0800 Subject: [PATCH 10/30] add Plugin typescript type --- src/index.tsx | 3 +++ src/types/plugins.ts | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/types/plugins.ts diff --git a/src/index.tsx b/src/index.tsx index c3be2aff..e9ab5b6a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,6 +9,7 @@ import { type AnnotationTargetExtended, } from "src/lib/annotation-helpers"; import { createOpenSeadragonRect } from "src/lib/openseadragon-helpers"; +import { type Plugin, type PluginInformationPanel } from "src/types/plugins"; export { Image, @@ -20,6 +21,8 @@ export { parseAnnotationsFromAnnotationResources, type AnnotationTargetExtended, createOpenSeadragonRect, + type Plugin, + type PluginInformationPanel, }; export default Viewer; diff --git a/src/types/plugins.ts b/src/types/plugins.ts new file mode 100644 index 00000000..59ab1aa3 --- /dev/null +++ b/src/types/plugins.ts @@ -0,0 +1,24 @@ +import { CanvasNormalized } from "@iiif/presentation-3"; +import { + ViewerContextStore, + ViewerConfigOptions, +} from "src/context/viewer-context"; + +export interface Plugin { + activeManifest: string; + canvas: CanvasNormalized; + viewerConfigOptions: ViewerConfigOptions; + openSeadragonViewer: OpenSeadragon.Viewer | null; + useViewerDispatch: () => ViewerContextStore; + useViewerState: () => ViewerContextStore; +} + +export interface PluginInformationPanel { + annotations?: any; + activeManifest: string; + canvas: CanvasNormalized; + viewerConfigOptions: ViewerConfigOptions; + openSeadragonViewer: OpenSeadragon.Viewer | null; + useViewerDispatch: () => ViewerContextStore; + useViewerState: () => ViewerContextStore; +} From e951790f6bbf64d5abeaf634dee66f43d3f0262f Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Thu, 7 Mar 2024 01:08:42 -0800 Subject: [PATCH 11/30] add menu section to plugins props --- src/components/Image/Controls/Controls.tsx | 43 ++++++++++++++++++++++ src/components/Viewer/Viewer/Viewer.tsx | 25 ------------- src/context/viewer-context.tsx | 6 ++- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/components/Image/Controls/Controls.tsx b/src/components/Image/Controls/Controls.tsx index 651ae2e5..3b6e32b6 100644 --- a/src/components/Image/Controls/Controls.tsx +++ b/src/components/Image/Controls/Controls.tsx @@ -2,6 +2,12 @@ import Button from "src/components/Image/Controls/Button"; import { Options } from "openseadragon"; import React from "react"; import { Wrapper } from "src/components/Image/Controls/Controls.styled"; +import { + ViewerContextStore, + useViewerState, + useViewerDispatch, +} from "src/context/viewer-context"; +import { CanvasNormalized } from "@iiif/presentation-3"; const ZoomIn = () => { return ( @@ -66,6 +72,42 @@ const Controls = ({ _cloverViewerHasPlaceholder: boolean; config: Options; }) => { + const viewerState: ViewerContextStore = useViewerState(); + const { + activeCanvas, + configOptions, + openSeadragonViewer, + plugins, + vault, + activeManifest, + } = viewerState; + + const canvas: CanvasNormalized = vault.get({ + id: activeCanvas, + type: "Canvas", + }); + + function renderPlugins() { + return plugins + .filter((plugin) => plugin.menu) + .map((plugin, i) => { + const PluginComponent = plugin.menu + ?.component as unknown as React.ElementType; + return ( + + ); + }); + } + return ( )} + {renderPlugins()} ); }; diff --git a/src/components/Viewer/Viewer/Viewer.tsx b/src/components/Viewer/Viewer/Viewer.tsx index b6842528..f26dd667 100644 --- a/src/components/Viewer/Viewer/Viewer.tsx +++ b/src/components/Viewer/Viewer/Viewer.tsx @@ -57,7 +57,6 @@ const Viewer: React.FC = ({ contentSearchVault, configOptions, openSeadragonViewer, - plugins, } = viewerState; const absoluteCanvasHeights = ["100%", "auto"]; @@ -90,11 +89,6 @@ const Viewer: React.FC = ({ [viewerDispatch], ); - const canvas: CanvasNormalized = vault.get({ - id: activeCanvas, - type: "Canvas", - }); - useEffect(() => { if (configOptions?.informationPanel?.open) { setInformationOpen(!isSmallViewport); @@ -190,24 +184,6 @@ const Viewer: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [openSeadragonViewer, contentSearchResource]); - function renderPlugins() { - return plugins.map((plugin, i) => { - const PluginComponent = plugin.component as unknown as React.ElementType; - return ( - - ); - }); - } - return ( = ({ manifestLabel={manifest.label as InternationalString} manifestId={manifest.id} /> - {activeCanvas && openSeadragonViewer && renderPlugins()} ; + menu?: { + component: React.ElementType; + componentProps?: Record; + }; informationPanel?: { component: React.ElementType; componentProps?: Record; From 6350c7dc4fbc679a1a4f531ef01ed2c050b8fb5c Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Thu, 7 Mar 2024 16:15:34 -0800 Subject: [PATCH 12/30] refactor plugin info panel so the panel can be shown or hidden --- docs/components/DynamicImports/Viewer.tsx | 4 +- .../InformationPanel/Annotation/Page.tsx | 59 +------- .../InformationPanel/InformationPanel.tsx | 127 +++++++++++------- src/components/Viewer/index.tsx | 4 +- src/context/viewer-context.tsx | 7 +- src/lib/plugin-helpers.ts | 57 ++++++++ 6 files changed, 146 insertions(+), 112 deletions(-) create mode 100644 src/lib/plugin-helpers.ts diff --git a/docs/components/DynamicImports/Viewer.tsx b/docs/components/DynamicImports/Viewer.tsx index 3fac3f09..bf4b6a47 100644 --- a/docs/components/DynamicImports/Viewer.tsx +++ b/docs/components/DynamicImports/Viewer.tsx @@ -1,7 +1,7 @@ import { type CustomDisplay, ViewerConfigOptions, - Plugin, + PluginConfig, } from "src/context/viewer-context"; import dynamic from "next/dynamic"; import { isDark } from "docs/lib/theme"; @@ -27,7 +27,7 @@ const CloverViewer = ({ options?: ViewerConfigOptions; customDisplays?: Array; iiifContentSearchQuery?: ContentSearchQuery; - plugins?: Array; + plugins?: Array; }) => { const router = useRouter(); const iiifResource = router.query["iiif-content"] diff --git a/src/components/Viewer/InformationPanel/Annotation/Page.tsx b/src/components/Viewer/InformationPanel/Annotation/Page.tsx index e392a4b1..b9bccd56 100644 --- a/src/components/Viewer/InformationPanel/Annotation/Page.tsx +++ b/src/components/Viewer/InformationPanel/Annotation/Page.tsx @@ -1,13 +1,8 @@ import { AnnotationNormalized, AnnotationPageNormalized, - CanvasNormalized, } from "@iiif/presentation-3"; -import { - ViewerContextStore, - useViewerState, - useViewerDispatch, -} from "src/context/viewer-context"; +import { ViewerContextStore, useViewerState } from "src/context/viewer-context"; import AnnotationItem from "src/components/Viewer/InformationPanel/Annotation/Item"; import { Group } from "src/components/Viewer/InformationPanel/Annotation/Item.styled"; @@ -18,14 +13,7 @@ type Props = { }; export const AnnotationPage: React.FC = ({ annotationPage }) => { const viewerState: ViewerContextStore = useViewerState(); - const { - vault, - openSeadragonViewer, - activeCanvas, - configOptions, - plugins, - activeManifest, - } = viewerState; + const { vault } = viewerState; if ( !annotationPage || @@ -40,49 +28,6 @@ export const AnnotationPage: React.FC = ({ annotationPage }) => { if (!annotations) return <>; - const canvas: CanvasNormalized = vault.get({ - id: activeCanvas, - type: "Canvas", - }); - - const plugin = plugins.find((plugin) => { - let match = false; - const annotationServer = - plugin.informationPanel?.componentProps?.annotationServer; - if (annotationServer) { - match = annotationServer === annotationPage.id; - } - - return match; - }); - - const PluginInformationPanelComponent = plugin?.informationPanel - ?.component as unknown as React.ElementType; - - if (PluginInformationPanelComponent) { - const annotationsWithBodies = annotations.map((annotation) => { - return { - ...annotation, - body: annotation.body.map((body) => vault.get(body.id)), - }; - }); - - return ( - - - - ); - } - return ( {annotations?.map((annotation) => ( diff --git a/src/components/Viewer/InformationPanel/InformationPanel.tsx b/src/components/Viewer/InformationPanel/InformationPanel.tsx index dd335f03..629b6fb3 100644 --- a/src/components/Viewer/InformationPanel/InformationPanel.tsx +++ b/src/components/Viewer/InformationPanel/InformationPanel.tsx @@ -10,6 +10,7 @@ import { ViewerContextStore, useViewerDispatch, useViewerState, + type PluginConfig, } from "src/context/viewer-context"; import AnnotationPage from "src/components/Viewer/InformationPanel/Annotation/Page"; @@ -22,6 +23,7 @@ import { CanvasNormalized, } from "@iiif/presentation-3"; import { Label } from "src/components/Primitives"; +import { setupPlugins, formatPluginAnnotations } from "src/lib/plugin-helpers"; const UserScrollTimeout = 1500; // 1500ms without a user-generated scroll event reverts to auto-scrolling @@ -66,31 +68,61 @@ export const InformationPanel: React.FC = ({ const renderAnnotation = informationPanel?.renderAnnotation; const renderContentSearch = informationPanel?.renderContentSearch; - const pluginsWithoutAnnotations = plugins.filter((plugin) => { - let match = false; - const annotationServer = - plugin.informationPanel?.componentProps?.annotationServer; - if (annotationServer === undefined) { - match = true; + const { pluginsWithInfoPanel, pluginsAnnotationPageIds } = + setupPlugins(plugins); + + function renderPluginLabel(plugin: PluginConfig, i: number) { + const annotations = formatPluginAnnotations(plugin, annotationResources); + + if ( + annotations.length === 0 && + plugin.informationPanel?.displayIfNoAnnotations === false + ) { + return <>; } - return match; - }); + const label = plugin.informationPanel?.label || { none: [plugin.id] }; + return ( + + + ); + } - function renderPluginInformationPanel(plugin) { + function renderPluginInformationPanel(plugin: PluginConfig, i: number) { const PluginInformationPanelComponent = plugin?.informationPanel ?.component as unknown as React.ElementType; + if (PluginInformationPanelComponent === undefined) { + return <>; + } + + const annotations = formatPluginAnnotations( + plugin, + annotationResources, + vault, + ); + + if ( + annotations.length === 0 && + plugin.informationPanel?.displayIfNoAnnotations === false + ) { + return <>; + } + return ( - + + + ); } @@ -167,23 +199,20 @@ export const InformationPanel: React.FC = ({ )} {renderAnnotation && annotationResources && - annotationResources.map((resource, i) => ( - - - ))} - - {pluginsWithoutAnnotations && - pluginsWithoutAnnotations.map((plugin, i) => ( - - - ))} + annotationResources + .filter((annotationPage) => { + return !pluginsAnnotationPageIds.includes(annotationPage.id); + }) + .map((resource, i) => ( + + + ))} + + {pluginsWithInfoPanel && + pluginsWithInfoPanel.map((plugin, i) => { + return renderPluginLabel(plugin, i); + })} {renderAbout && ( @@ -203,20 +232,22 @@ export const InformationPanel: React.FC = ({ )} {renderAnnotation && annotationResources && - annotationResources.map((annotationPage) => { - return ( - - - - ); - })} + annotationResources + .filter((annotationPage) => { + return !pluginsAnnotationPageIds.includes(annotationPage.id); + }) + .map((annotationPage) => { + return ( + + + + ); + })} - {pluginsWithoutAnnotations && - pluginsWithoutAnnotations.map((plugin, i) => ( - - {renderPluginInformationPanel(plugin)} - - ))} + {pluginsWithInfoPanel && + pluginsWithInfoPanel.map((plugin, i) => + renderPluginInformationPanel(plugin, i), + )} ); diff --git a/src/components/Viewer/index.tsx b/src/components/Viewer/index.tsx index cf1e3f77..5f113bf6 100644 --- a/src/components/Viewer/index.tsx +++ b/src/components/Viewer/index.tsx @@ -8,7 +8,7 @@ import { useViewerDispatch, useViewerState, CustomDisplay, - Plugin, + PluginConfig, } from "src/context/viewer-context"; import { Vault } from "@iiif/vault"; @@ -25,7 +25,7 @@ import { ContentSearchQuery } from "src/types/annotations"; export interface CloverViewerProps { canvasIdCallback?: (arg0: string) => void; customDisplays?: Array; - plugins?: Array; + plugins?: Array; customTheme?: any; iiifContent: string; id?: string; diff --git a/src/context/viewer-context.tsx b/src/context/viewer-context.tsx index 1a6339fe..73574a68 100644 --- a/src/context/viewer-context.tsx +++ b/src/context/viewer-context.tsx @@ -143,7 +143,7 @@ export type CustomDisplay = { paintingFormat?: string[]; }; }; -export type Plugin = { +export type PluginConfig = { id: string; menu?: { component: React.ElementType; @@ -152,7 +152,8 @@ export type Plugin = { informationPanel?: { component: React.ElementType; componentProps?: Record; - label?: InternationalString; + label: InternationalString; + displayIfNoAnnotations?: boolean; }; }; @@ -163,7 +164,7 @@ export interface ViewerContextStore { collection?: CollectionNormalized | Record; configOptions: ViewerConfigOptions; customDisplays: Array; - plugins: Array; + plugins: Array; isAutoScrollEnabled?: boolean; isAutoScrolling?: boolean; isInformationOpen: boolean; diff --git a/src/lib/plugin-helpers.ts b/src/lib/plugin-helpers.ts new file mode 100644 index 00000000..d196a25c --- /dev/null +++ b/src/lib/plugin-helpers.ts @@ -0,0 +1,57 @@ +import { AnnotationNormalized, Reference } from "@iiif/presentation-3"; +import { type PluginConfig } from "src/context/viewer-context"; +import { AnnotationResources } from "src/types/annotations"; +import { type Vault } from "@iiif/vault"; + +export function setupPlugins(plugins: PluginConfig[]) { + const pluginsWithInfoPanel: PluginConfig[] = []; + const pluginsAnnotationPageIds: string[] = []; + plugins.forEach((plugin) => { + if (plugin.informationPanel?.component) { + pluginsWithInfoPanel.push(plugin); + } + const annotationPageId = plugin?.informationPanel?.componentProps + ?.annotationServer as string; + if (annotationPageId) { + pluginsAnnotationPageIds.push(annotationPageId); + } + }); + + return { pluginsWithInfoPanel, pluginsAnnotationPageIds }; +} + +export function formatPluginAnnotations( + plugin: PluginConfig, + annotationResources: AnnotationResources | undefined, + vault: Vault | undefined = undefined, +) { + const annotationPageId = + plugin?.informationPanel?.componentProps?.annotationServer; + + let annotations: (AnnotationNormalized | Reference<"Annotation">)[] = []; + if ( + annotationPageId && + annotationResources && + annotationResources.length > 0 + ) { + const annotationPage = annotationResources?.find((resource) => { + return resource.id === annotationPageId; + }); + if (annotationPage) { + if (vault) { + annotations = annotationPage.items.map((item) => { + const annotation = vault.get(item.id) as AnnotationNormalized; + + return { + ...annotation, + body: annotation.body.map((body) => vault.get(body.id)), + }; + }); + } else { + annotations = annotationPage.items; + } + } + } + + return annotations; +} From d83498ba25984841ea3128bd60e857c623236c3b Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Fri, 8 Mar 2024 17:41:21 -0800 Subject: [PATCH 13/30] update build process to include annotation and openseadragon helpers --- build/build.mjs | 16 +++++++++++++++- build/root.mjs | 18 ++++++++++++++++-- build/root.umd.js | 8 ++++++++ package.json | 10 ++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/build/build.mjs b/build/build.mjs index 511b6994..7071d12a 100644 --- a/build/build.mjs +++ b/build/build.mjs @@ -11,7 +11,7 @@ const buildOptions = { entry: "./src/components/Image/index.tsx", fileName: "index", }, - }, + }, primitives: { lib: { name: "CloverIIIFPrimitives", @@ -38,6 +38,20 @@ const buildOptions = { name: "CloverIIIFScroll", entry: "./src/components/Scroll/index.tsx", fileName: "index", + } + }, + 'annotation-helpers': { + lib: { + name: "CloverIIIFAnnotationHelpers", + entry: "./src/lib/annotation-helpers.ts", + fileName: "index", + }, + }, + 'openseadragon-helpers': { + lib: { + name: "CloverIIIFOpenseadragonHelpers", + entry: "./src/lib/openseadragon-helpers.ts", + fileName: "index", }, }, }; diff --git a/build/root.mjs b/build/root.mjs index be57bcf0..0f85d2d6 100644 --- a/build/root.mjs +++ b/build/root.mjs @@ -3,7 +3,21 @@ import Primitives from "./primitives"; import Scroll from "./scroll"; import Slider from "./slider"; import Viewer from "./viewer"; +import { + parseAnnotationTarget, + parseAnnotationsFromAnnotationResources, +} from "./annotation-helpers"; +import { createOpenSeadragonRect } from "./openseadragon-helpers"; -export { Image, Primitives, Scroll, Slider, Viewer }; +export { + Image, + Primitives, + Scroll, + Slider, + Viewer, + parseAnnotationTarget, + parseAnnotationsFromAnnotationResources, + createOpenSeadragonRect +}; -export default Viewer; +export default Viewer; diff --git a/build/root.umd.js b/build/root.umd.js index ed36118f..f6605100 100644 --- a/build/root.umd.js +++ b/build/root.umd.js @@ -5,6 +5,11 @@ const Primitives = require("./primitives"); const Scroll = require("./scroll"); const Slider = require("./slider"); const Viewer = require("./viewer"); +const { + parseAnnotationTarget, + parseAnnotationsFromAnnotationResources +} = require("./annotation_helpers"); +const { createOpenSeadragonRect } = require("./openseadragon-helpers"); module.exports = { default: Viewer, @@ -13,4 +18,7 @@ module.exports = { Scroll, Slider, Viewer, + parseAnnotationTarget, + parseAnnotationsFromAnnotationResources, + createOpenSeadragonRect }; diff --git a/package.json b/package.json index ff110f7e..2ead45a0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,16 @@ "import": "./dist/viewer/index.mjs", "require": "./dist/viewer/index.umd.js", "types": "./dist/viewer/index.d.ts" + }, + "./annotation-helpers": { + "import": "./dist/annotation-helpers/index.mjs", + "require": "./dist/annotation-helpers/index.umd.js", + "types": "./dist/annotation-helpers/index.d.ts" + }, + "./openseadragon-helpers": { + "import": "./dist/openseadragon-helpers/index.mjs", + "require": "./dist/openseadragon-helpers/index.umd.js", + "types": "./dist/openseadragon-helpers/index.d.ts" } }, "types": "./dist/index.d.ts", From d535797b03dd8f97377ccb5b7e668e1388fca604 Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Sat, 9 Mar 2024 19:18:17 -0800 Subject: [PATCH 14/30] update types for plugins --- src/types/plugins.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/plugins.ts b/src/types/plugins.ts index 59ab1aa3..c36bf923 100644 --- a/src/types/plugins.ts +++ b/src/types/plugins.ts @@ -1,4 +1,4 @@ -import { CanvasNormalized } from "@iiif/presentation-3"; +import { CanvasNormalized, Annotation } from "@iiif/presentation-3"; import { ViewerContextStore, ViewerConfigOptions, @@ -14,7 +14,7 @@ export interface Plugin { } export interface PluginInformationPanel { - annotations?: any; + annotations?: Annotation[]; activeManifest: string; canvas: CanvasNormalized; viewerConfigOptions: ViewerConfigOptions; From 04fb1b2252f7f703f5212e8b49cc73358d9bba60 Mon Sep 17 00:00:00 2001 From: Wai-Yin Kwan Date: Sat, 9 Mar 2024 19:28:04 -0800 Subject: [PATCH 15/30] improve error handling - add console.log, ErrorFallback, try...catch --- .../InformationPanel/InformationPanel.tsx | 35 +++++++++++-------- src/components/Viewer/Viewer/Content.tsx | 18 ++++++---- src/lib/plugin-helpers.ts | 19 ++++++++-- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/components/Viewer/InformationPanel/InformationPanel.tsx b/src/components/Viewer/InformationPanel/InformationPanel.tsx index 629b6fb3..a19a74a4 100644 --- a/src/components/Viewer/InformationPanel/InformationPanel.tsx +++ b/src/components/Viewer/InformationPanel/InformationPanel.tsx @@ -24,6 +24,9 @@ import { } from "@iiif/presentation-3"; import { Label } from "src/components/Primitives"; import { setupPlugins, formatPluginAnnotations } from "src/lib/plugin-helpers"; +import ErrorFallback from "../../UI/ErrorFallback/ErrorFallback"; + +import { ErrorBoundary } from "react-error-boundary"; const UserScrollTimeout = 1500; // 1500ms without a user-generated scroll event reverts to auto-scrolling @@ -57,15 +60,15 @@ export const InformationPanel: React.FC = ({ } = viewerState; const { informationPanel } = configOptions; + const [activeResource, setActiveResource] = useState(); + + const renderAbout = informationPanel?.renderAbout; + const renderAnnotation = informationPanel?.renderAnnotation; const canvas: CanvasNormalized = vault.get({ id: activeCanvas, type: "Canvas", }); - const [activeResource, setActiveResource] = useState(); - - const renderAbout = informationPanel?.renderAbout; - const renderAnnotation = informationPanel?.renderAnnotation; const renderContentSearch = informationPanel?.renderContentSearch; const { pluginsWithInfoPanel, pluginsAnnotationPageIds } = @@ -73,7 +76,6 @@ export const InformationPanel: React.FC = ({ function renderPluginLabel(plugin: PluginConfig, i: number) { const annotations = formatPluginAnnotations(plugin, annotationResources); - if ( annotations.length === 0 && plugin.informationPanel?.displayIfNoAnnotations === false @@ -82,6 +84,7 @@ export const InformationPanel: React.FC = ({ } const label = plugin.informationPanel?.label || { none: [plugin.id] }; + return (