Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8,457 changes: 8,457 additions & 0 deletions public/manifest/behavior/paged-rtl.json

Large diffs are not rendered by default.

62 changes: 44 additions & 18 deletions src/components/Viewer/Media/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ControlsProps {
handleFilter: (arg0: string) => void;
activeIndex: number;
canvasLength: number;
isRtlPaged?: boolean;
}

const PreviousIcon = ({ title }: { title: string }) => {
Expand Down Expand Up @@ -74,6 +75,7 @@ const Controls: React.FC<ControlsProps> = ({
handleFilter,
activeIndex,
canvasLength,
isRtlPaged = false,
}) => {
const [toggleFilter, setToggleFilter] = useState<boolean>(false);
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(false);
Expand Down Expand Up @@ -126,24 +128,48 @@ const Controls: React.FC<ControlsProps> = ({
/>
)}
{!toggleFilter && (
<Direction className="clover-viewer-media-navigation">
<Button
onClick={() => handleCanvasToggle(-1)}
disabled={isPreviousDisabled}
type="button"
>
<PreviousIcon title={t("commonPrevious")} />
</Button>
<span>
{activeIndex + 1} <em>/</em> {canvasLength}
</span>
<Button
onClick={() => handleCanvasToggle(1)}
disabled={isNextDisabled}
type="button"
>
<NextIcon title={t("commonNext")} />
</Button>
<Direction className="clover-viewer-media-navigation" data-rtl-paged={isRtlPaged}>
{isRtlPaged ? (
<>
<Button
onClick={() => handleCanvasToggle(1)}
disabled={isNextDisabled}
type="button"
>
<PreviousIcon title={t("commonNext")} />
</Button>
<span>
{activeIndex + 1} <em>/</em> {canvasLength}
</span>
<Button
onClick={() => handleCanvasToggle(-1)}
disabled={isPreviousDisabled}
type="button"
>
<NextIcon title={t("commonPrevious")} />
</Button>
</>
) : (
<>
<Button
onClick={() => handleCanvasToggle(-1)}
disabled={isPreviousDisabled}
type="button"
>
<PreviousIcon title={t("commonPrevious")} />
</Button>
<span>
{activeIndex + 1} <em>/</em> {canvasLength}
</span>
<Button
onClick={() => handleCanvasToggle(1)}
disabled={isNextDisabled}
type="button"
>
<NextIcon title={t("commonNext")} />
</Button>
</>
)}
</Direction>
)}
<Button
Expand Down
10 changes: 9 additions & 1 deletion src/components/Viewer/Media/Media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ const Media: React.FC<MediaProps> = ({ items }) => {
const { t } = useCloverTranslation();
const dispatch: any = useViewerDispatch();
const state: ViewerContextStore = useViewerState();
const { activeCanvas, vault, sequence } = state;
const { activeCanvas, isPaged, vault, sequence, viewingDirection } = state;

/**
* Determine if RTL paged navigation should be used
*/
const isRtlPaged = isPaged && viewingDirection === "right-to-left";

const [filter, setFilter] = useState<string>("");
const [mediaItems, setMediaItems] = useState<Array<CanvasEntity>>([]);
Expand Down Expand Up @@ -110,14 +115,17 @@ const Media: React.FC<MediaProps> = ({ items }) => {
handleCanvasToggle={handleCanvasToggle}
activeIndex={activeIndex}
canvasLength={items.length}
isRtlPaged={isRtlPaged}
/>
<StyledSequence
aria-label={t("media.selectItem")}
data-testid="media"
data-active-canvas={items[activeIndex].id}
data-canvas-length={items.length}
data-filter={filter}
data-rtl-paged={isRtlPaged}
ref={scrollRef}
style={{ direction: isRtlPaged ? "rtl" : "ltr" }}
>
{sequence[1]
.filter((groups) => {
Expand Down
21 changes: 19 additions & 2 deletions src/components/Viewer/Painting/Painting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,19 @@ const Painting: React.FC<PaintingProps> = ({
customDisplays,
contentStateAnnotation,
informationPanelResource,
isPaged,
openSeadragonViewer,
vault,
viewerId,
viewingDirection,
visibleCanvases,
} = useViewerState();

/**
* Determine if canvases should be displayed in reverse order
* for right-to-left viewing direction when behavior is paged
*/
const isRtlPaged = isPaged && viewingDirection === "right-to-left";
const [annotations, setAnnotations] = React.useState<
Array<{
annotation: Annotation;
Expand Down Expand Up @@ -208,15 +216,23 @@ const Painting: React.FC<PaintingProps> = ({
if (isMedia) {
return;
} else {
const body = visibleCanvases
/**
* For RTL paged content, we reverse the order of visible canvases
* so that the rightmost canvas appears on the left side of the display
*/
const orderedCanvases = isRtlPaged
? [...visibleCanvases].reverse()
: visibleCanvases;

const body = orderedCanvases
.map((canvas) => {
const canvasId = canvas.id;
const painting = getPaintingResource(vault, canvasId);
return painting ? painting[annotationIndex] : undefined;
})
.filter(Boolean) as LabeledIIIFExternalWebResource[];

const placeholders = visibleCanvases
const placeholders = orderedCanvases
.map((entry) => {
const canvasId = entry.id;

Expand All @@ -239,6 +255,7 @@ const Painting: React.FC<PaintingProps> = ({
}, [
annotationIndex,
activeCanvas,
isRtlPaged,
visibleCanvases,
isMedia,
normalizedCanvas,
Expand Down
5 changes: 5 additions & 0 deletions src/components/Viewer/Painting/Placeholder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { InternationalString } from "@iiif/presentation-3";
import { PlaceholderStyled } from "./Placeholder.styled";
import { useViewerState } from "src/context/viewer-context";

/**
* Note: The items array passed to this component is already ordered
* correctly for RTL paged content by the parent Painting component.
*/

interface Props {
isActive: boolean;
isMedia: boolean;
Expand Down
26 changes: 4 additions & 22 deletions src/components/Viewer/Player/Player.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { LabeledIIIFExternalWebResource } from "src/types/presentation-3";
import Player from "src/components/Viewer/Player/Player";
import React from "react";
import { Vault } from "@iiif/helpers/vault";
import { ViewerProvider } from "src/context/viewer-context";
import { ViewerProvider, defaultState } from "src/context/viewer-context";
import manifestSimpleAudio from "src/fixtures/viewer/player/manifest-simple-audio.json";
import manifestStreaming from "src/fixtures/viewer/player/manifest-streaming-audio.json";

Expand Down Expand Up @@ -60,18 +60,12 @@ describe("Player component", () => {
render(
<ViewerProvider
initialState={{
...defaultState,
activeCanvas:
"https://dcapi.rdc-staging.library.northwestern.edu/api/v2/works/d2a423b1-6b5e-45cb-9956-46a99cd62cfd?as=iiif/canvas/access/0",
activeManifest:
"https://dcapi.rdc-staging.library.northwestern.edu/api/v2/works/d2a423b1-6b5e-45cb-9956-46a99cd62cfd?as=iiif",
collection: {},
configOptions: {},
customDisplays: [],
plugins: [],
isInformationOpen: false,
isLoaded: false,
vault,
openSeadragonViewer: null,
}}
>
<Player {...props} />
Expand Down Expand Up @@ -121,18 +115,12 @@ describe("Player component", () => {
render(
<ViewerProvider
initialState={{
...defaultState,
activeCanvas:
"https://dcapi.rdc-staging.library.northwestern.edu/api/v2/works/d2a423b1-6b5e-45cb-9956-46a99cd62cfd?as=iiif/canvas/access/0",
activeManifest:
"https://dcapi.rdc-staging.library.northwestern.edu/api/v2/works/d2a423b1-6b5e-45cb-9956-46a99cd62cfd?as=iiif",
collection: {},
configOptions: {},
customDisplays: [],
plugins: [],
isInformationOpen: false,
isLoaded: false,
vault,
openSeadragonViewer: null,
}}
>
<Player {...props} />
Expand Down Expand Up @@ -179,18 +167,12 @@ describe("Player component", () => {
render(
<ViewerProvider
initialState={{
...defaultState,
activeCanvas:
"https://iiif.io/api/cookbook/recipe/0002-mvm-audio/canvas",
activeManifest:
"https://iiif.io/api/cookbook/recipe/0002-mvm-audio/manifest.json",
collection: {},
configOptions: {},
customDisplays: [],
plugins: [],
isInformationOpen: false,
isLoaded: false,
vault,
openSeadragonViewer: null,
}}
>
<Player {...props} />
Expand Down
21 changes: 21 additions & 0 deletions src/components/Viewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,27 @@ const RenderViewer: React.FC<CloverViewerProps> = ({
type: "updateManifestSequence",
sequence,
});

/**
* Extract viewingDirection from manifest (defaults to left-to-right)
* and check if behavior includes "paged"
*/
// @ts-ignore - viewingDirection exists on IIIF manifest but may not be typed
const viewingDirection = data.viewingDirection || "left-to-right";
// @ts-ignore - behavior exists on IIIF manifest but may not be typed
const behavior = data.behavior || [];
const isPaged = Array.isArray(behavior)
? behavior.includes("paged")
: behavior === "paged";

dispatch({
type: "updateViewingDirection",
viewingDirection,
});
dispatch({
type: "updateIsPaged",
isPaged,
});
})
.catch((error: Error) => {
console.error(`Manifest failed to load: ${error}`);
Expand Down
24 changes: 24 additions & 0 deletions src/context/viewer-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ export type PluginConfig = {
};
};

export type ViewingDirection =
| "left-to-right"
| "right-to-left"
| "top-to-bottom"
| "bottom-to-top";

export interface ViewerContextStore {
activeCanvas: string;
activeManifest: string;
Expand All @@ -193,9 +199,11 @@ export interface ViewerContextStore {
isAutoScrolling?: boolean;
isInformationOpen: boolean;
isLoaded: boolean;
isPaged: boolean;
isUserScrolling?: number | undefined;
sequence: [Reference<"Canvas">[], number[][]];
vault: Vault;
viewingDirection: ViewingDirection;
openSeadragonViewer: OpenSeadragon.Viewer | null;
openSeadragonId?: string;
viewerId?: string;
Expand All @@ -215,12 +223,14 @@ export interface ViewerAction {
isAutoScrolling: boolean;
isInformationOpen: boolean;
isLoaded: boolean;
isPaged: boolean;
isUserScrolling: number | undefined;
manifestId: string;
OSDImageLoaded?: boolean;
player: HTMLVideoElement | HTMLAudioElement | null;
sequence: [Reference<"Canvas">[], number[][]];
vault: Vault;
viewingDirection: ViewingDirection;
openSeadragonViewer: OpenSeadragon.Viewer;
viewerId: string;
visibleCanvases: Array<Reference<"Canvas">>;
Expand Down Expand Up @@ -304,9 +314,11 @@ export const createDefaultState = (): ViewerContextStore => ({
// Respect explicit false; default to true only when undefined
isInformationOpen: defaultConfigOptions?.informationPanel?.open ?? true,
isLoaded: false,
isPaged: false,
isUserScrolling: undefined,
sequence: [[], []],
vault: new Vault(),
viewingDirection: "left-to-right",
openSeadragonViewer: null,
viewerId: uuidv4(),
visibleCanvases: [],
Expand Down Expand Up @@ -438,6 +450,18 @@ function viewerReducer(state: ViewerContextStore, action: ViewerAction) {
visibleCanvases: action.visibleCanvases,
};
}
case "updateViewingDirection": {
return {
...state,
viewingDirection: action.viewingDirection,
};
}
case "updateIsPaged": {
return {
...state,
isPaged: action.isPaged,
};
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
Expand Down