+ {#if pageWidth > remToPx(32) || !document.edit_access}
+
+ (pageShareOpen = true)}>
+ {$_("dialog.share")}
+
+ {#if document.edit_access}
+
+
(pageNote = true)}>
+ {$_("annotate.cta.add-note")}
+
+
+
(editSection = true)}>
+ {#if section}
+ {$_("annotate.cta.edit-section")}
+ {:else}
+ {$_("annotate.cta.add-section")}
+ {/if}
+
+
+ {/if}
+
+ {:else}
+
+
+
+
+ {/if}
+
+{#if pageShareOpen}
+
- {#await text}
-
- {#each Array(total).fill(null) as p, n}
-
-
-
- {/each}
- {:then { pages }}
- {#each pages as { page, contents }}
-
- {/each}
- {/await}
+
+ {#each text?.pages as { page, contents }}
+
+
+ {@html highlight(contents, query)}
+
+
+ {/each}
diff --git a/src/lib/components/viewer/TextPage.svelte b/src/lib/components/viewer/TextPage.svelte
deleted file mode 100644
index 4434c89c2..000000000
--- a/src/lib/components/viewer/TextPage.svelte
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
- {@html highlight(contents, query)}
-
-
-
-
diff --git a/src/lib/components/viewer/Viewer.svelte b/src/lib/components/viewer/Viewer.svelte
index 547b73985..3b56f6810 100644
--- a/src/lib/components/viewer/Viewer.svelte
+++ b/src/lib/components/viewer/Viewer.svelte
@@ -1,9 +1,10 @@
-
+
+
+
+
+
+ {#if shouldPreload($currentMode)}
+
+ {/if}
+
+
+
diff --git a/src/lib/components/viewer/Zoom.svelte b/src/lib/components/viewer/Zoom.svelte
index 585a29198..54b190ce5 100644
--- a/src/lib/components/viewer/Zoom.svelte
+++ b/src/lib/components/viewer/Zoom.svelte
@@ -1,118 +1,31 @@
-
+ import { getDefaultZoom, getZoomLevels } from "@/lib/utils/viewer";
+ import { getCurrentMode, getZoom } from "./ViewerContext.svelte";
-
{#if zoomLevels.length}
diff --git a/src/lib/components/viewer/stories/AnnotationLayer.stories.svelte b/src/lib/components/viewer/stories/AnnotationLayer.stories.svelte
new file mode 100644
index 000000000..82060b41a
--- /dev/null
+++ b/src/lib/components/viewer/stories/AnnotationLayer.stories.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+
+ {#each sizes as [width, height], page_number}
+
+
+
+ {/each}
+
+
+
+
+
+
+
+ {#each sizes as [width, height], page_number}
+
+
+
+ {/each}
+
+
+
+
+
diff --git a/src/lib/components/viewer/stories/AnnotationPane.stories.svelte b/src/lib/components/viewer/stories/AnnotationPane.stories.svelte
deleted file mode 100644
index 80914e416..000000000
--- a/src/lib/components/viewer/stories/AnnotationPane.stories.svelte
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
- {#each sizes as [width, height], page_number}
-
-
-
- {/each}
-
-
-
-
diff --git a/src/lib/components/viewer/toolbars/stories/AnnotationToolbar.stories.svelte b/src/lib/components/viewer/stories/AnnotationToolbar.stories.svelte
similarity index 100%
rename from src/lib/components/viewer/toolbars/stories/AnnotationToolbar.stories.svelte
rename to src/lib/components/viewer/stories/AnnotationToolbar.stories.svelte
diff --git a/src/lib/components/viewer/stories/Grid.stories.svelte b/src/lib/components/viewer/stories/Grid.stories.svelte
new file mode 100644
index 000000000..cca03f58b
--- /dev/null
+++ b/src/lib/components/viewer/stories/Grid.stories.svelte
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/viewer/stories/Note.stories.svelte b/src/lib/components/viewer/stories/Note.stories.svelte
index ddf39efd3..e2f71989d 100644
--- a/src/lib/components/viewer/stories/Note.stories.svelte
+++ b/src/lib/components/viewer/stories/Note.stories.svelte
@@ -7,12 +7,14 @@
import.meta.url,
).href;
- import { Story } from "@storybook/addon-svelte-csf";
- import { setContext } from "svelte";
+ import { Story, Template } from "@storybook/addon-svelte-csf";
+ import ViewerContext from "../ViewerContext.svelte";
import Note from "../Note.svelte";
import doc from "@/test/fixtures/documents/document-expanded.json";
import pdfFile from "@/test/fixtures/documents/examples/agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government.pdf";
+ import { writable } from "svelte/store";
+ import { pdfUrl } from "@/lib/api/documents";
const document = doc as Document;
const notes = document.notes as NoteType[];
@@ -36,35 +38,31 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {#await load(url) then pdf}
-
- {/await}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/viewer/stories/Notes.stories.svelte b/src/lib/components/viewer/stories/Notes.stories.svelte
index 2c5fdbbd6..e78a18648 100644
--- a/src/lib/components/viewer/stories/Notes.stories.svelte
+++ b/src/lib/components/viewer/stories/Notes.stories.svelte
@@ -1,9 +1,9 @@
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
-
-
-
+
diff --git a/src/lib/components/viewer/stories/NotesPane.stories.svelte b/src/lib/components/viewer/stories/NotesPane.stories.svelte
deleted file mode 100644
index 91138392f..000000000
--- a/src/lib/components/viewer/stories/NotesPane.stories.svelte
+++ /dev/null
@@ -1,76 +0,0 @@
-
-
-
-
-
-
- {#await load(url) then pdf}
- {#each sizes as [width, height], page_number}
-
-
-
- {/each}
- {/await}
-
-
-
-
diff --git a/src/lib/components/viewer/stories/PDF.stories.svelte b/src/lib/components/viewer/stories/PDF.stories.svelte
index 6d8ad0735..aee2b10ee 100644
--- a/src/lib/components/viewer/stories/PDF.stories.svelte
+++ b/src/lib/components/viewer/stories/PDF.stories.svelte
@@ -1,15 +1,18 @@
-
-
-
-
-
+
+
+
+
+
-
-
-
+
-
-
-
+
-
-
-
+
+
+
-
-
+ args={{
+ ...args,
+ context: { ...args.context, asset_url: new URL(loadingUrl) },
+ }}
+/>
-
-
-
+
+ res(
+ ctx.status(400, "Ambiguous Error"),
+ ctx.json("Something went horribly wrong."),
+ ),
+ ),
+ ],
+ },
+ }}
+ args={{
+ ...args,
+ context: { ...args.context, asset_url: new URL(loadingUrl) },
+ }}
+/>
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/viewer/stories/PDFPage.stories.svelte b/src/lib/components/viewer/stories/PDFPage.stories.svelte
index d0e4f9ec5..3aec5ae6c 100644
--- a/src/lib/components/viewer/stories/PDFPage.stories.svelte
+++ b/src/lib/components/viewer/stories/PDFPage.stories.svelte
@@ -3,7 +3,7 @@
import { Story } from "@storybook/addon-svelte-csf";
import PdfPage from "../PDFPage.svelte";
- import * as pdfjs from "pdfjs-dist/build/pdf.mjs";
+ import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs";
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.mjs",
import.meta.url,
@@ -13,6 +13,8 @@
import doc from "@/test/fixtures/documents/examples/the-santa-anas.json";
import textPositions from "@/test/fixtures/documents/examples/the-santa-anas-p1.position.json";
import pdfFile from "@/test/fixtures/documents/examples/the-santa-anas.pdf";
+ import ViewerContext from "../ViewerContext.svelte";
+ import { writable } from "svelte/store";
const document = { ...doc, edit_access: true } as Document;
@@ -27,92 +29,43 @@
const query = "los angeles";
const url = new URL(pdfFile, import.meta.url);
- // sections
- const section: Section = { id: 1, page_number: 1, title: "Something uneasy" };
- const long_section: Section = {
- id: 1,
- page_number: 1,
- title:
- "What it means is that tonight a Santa Ana will begin to blow, a hot wind from the northeast whining down through the Cajon and SanGorgonio Passes, blowing up sand storms out along Route 66, drying the hills andthe nerves to flash point.",
- };
-
- async function load(url: URL) {
+ async function load(url: URL): Promise {
return pdfjs.getDocument(url).promise;
}
- {#await load(url) then pdf}
-
- {/await}
+
+
+
- {#await load(url) then pdf}
-
- {/await}
+
+
+
- {#await load(url) then pdf}
+
- {/await}
+
- {#await load(url) then pdf}
-
- {/await}
-
-
-
- {#await load(url) then pdf}
-
- {/await}
+
+
+
- {#await load(url) then pdf}
-
- {/await}
+
+
+
diff --git a/src/lib/components/viewer/toolbars/stories/PaginationToolbar.stories.svelte b/src/lib/components/viewer/stories/PaginationToolbar.stories.svelte
similarity index 100%
rename from src/lib/components/viewer/toolbars/stories/PaginationToolbar.stories.svelte
rename to src/lib/components/viewer/stories/PaginationToolbar.stories.svelte
diff --git a/src/lib/components/viewer/toolbars/stories/ReadingToolbar.stories.svelte b/src/lib/components/viewer/stories/ReadingToolbar.stories.svelte
similarity index 100%
rename from src/lib/components/viewer/toolbars/stories/ReadingToolbar.stories.svelte
rename to src/lib/components/viewer/stories/ReadingToolbar.stories.svelte
diff --git a/src/lib/components/viewer/stories/RedactionPane.stories.svelte b/src/lib/components/viewer/stories/RedactionLayer.stories.svelte
similarity index 79%
rename from src/lib/components/viewer/stories/RedactionPane.stories.svelte
rename to src/lib/components/viewer/stories/RedactionLayer.stories.svelte
index 17e503eed..c7a91690e 100644
--- a/src/lib/components/viewer/stories/RedactionPane.stories.svelte
+++ b/src/lib/components/viewer/stories/RedactionLayer.stories.svelte
@@ -1,13 +1,13 @@
@@ -28,7 +28,7 @@
{#each redacted as page}
-
+
{/each}
diff --git a/src/lib/components/viewer/toolbars/stories/RedactionToolbar.stories.svelte b/src/lib/components/viewer/stories/RedactionToolbar.stories.svelte
similarity index 100%
rename from src/lib/components/viewer/toolbars/stories/RedactionToolbar.stories.svelte
rename to src/lib/components/viewer/stories/RedactionToolbar.stories.svelte
diff --git a/src/lib/components/viewer/stories/Section.stories.svelte b/src/lib/components/viewer/stories/Section.stories.svelte
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/lib/components/viewer/stories/Text.stories.svelte b/src/lib/components/viewer/stories/Text.stories.svelte
index 130f4c05b..686cde891 100644
--- a/src/lib/components/viewer/stories/Text.stories.svelte
+++ b/src/lib/components/viewer/stories/Text.stories.svelte
@@ -9,9 +9,12 @@
};
import { document } from "@/test/fixtures/documents";
- import txt from "@/test/fixtures/documents/document.txt.json";
+ import text from "@/test/fixtures/documents/document.txt.json";
+ import ViewerContext from "../ViewerContext.svelte";
-
+
+
+
diff --git a/src/lib/components/viewer/stories/TextPage.stories.svelte b/src/lib/components/viewer/stories/TextPage.stories.svelte
deleted file mode 100644
index 1aae7d49a..000000000
--- a/src/lib/components/viewer/stories/TextPage.stories.svelte
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/lib/components/viewer/stories/ThumbnailGrid.stories.svelte b/src/lib/components/viewer/stories/ThumbnailGrid.stories.svelte
deleted file mode 100644
index 8a02ea6a3..000000000
--- a/src/lib/components/viewer/stories/ThumbnailGrid.stories.svelte
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/lib/components/viewer/stories/Viewer.stories.svelte b/src/lib/components/viewer/stories/Viewer.stories.svelte
index 82b8f613a..3d66f8116 100644
--- a/src/lib/components/viewer/stories/Viewer.stories.svelte
+++ b/src/lib/components/viewer/stories/Viewer.stories.svelte
@@ -2,11 +2,12 @@
import type { Document, DocumentText, ViewerMode } from "$lib/api/types";
import { Story, Template } from "@storybook/addon-svelte-csf";
- import ViewerContextDecorator from "@/../.storybook/decorators/ViewerContextDecorator.svelte";
+ import ViewerContext from "../ViewerContext.svelte";
import Viewer from "../Viewer.svelte";
import doc from "@/test/fixtures/documents/document-expanded.json";
import txt from "@/test/fixtures/documents/document.txt.json";
+ import Note from "../Note.svelte";
const document = doc as Document;
@@ -34,9 +35,9 @@
-
-
-
+
+
+
@@ -44,7 +45,11 @@
name="Edit Access"
args={{
...args,
- document: { ...document, edit_access: true },
+ document: {
+ ...document,
+ edit_access: true,
+ notes: document.notes.map((note) => ({ ...note, edit_access: true })),
+ },
}}
/>
- import { Story } from "@storybook/addon-svelte-csf";
+ import { Template, Story } from "@storybook/addon-svelte-csf";
import Zoom from "../Zoom.svelte";
export const meta = {
@@ -10,16 +10,18 @@
},
tags: ["autodocs"],
};
+
+ let args = {
+ mode: "document",
+ };
-
-
-
+
+
+
+
+
-
-
-
+
-
-
-
+
diff --git a/src/lib/load/document.ts b/src/lib/load/document.ts
new file mode 100644
index 000000000..99ceefe95
--- /dev/null
+++ b/src/lib/load/document.ts
@@ -0,0 +1,38 @@
+import type { ViewerMode } from "$lib/api/types";
+
+import { error } from "@sveltejs/kit";
+import * as documents from "$lib/api/documents";
+
+interface Load {
+ fetch: typeof globalThis.fetch;
+ params: { id: string };
+ url: URL;
+}
+
+export default async function load({ fetch, params, url }: Load) {
+ const { data: document, error: err } = await documents.get(+params.id, fetch);
+
+ if (err) {
+ throw error(err.status, err.message);
+ }
+
+ if (!document) {
+ throw error(404, "Document not found");
+ }
+
+ let mode: ViewerMode =
+ (url.searchParams.get("mode") as ViewerMode) ?? "document";
+ const text = await documents.text(document, fetch);
+ const asset_url = await documents.assetUrl(document, fetch);
+
+ if (!documents.MODES.has(mode)) {
+ mode = documents.MODES[0];
+ }
+
+ return {
+ document,
+ text,
+ asset_url,
+ mode,
+ };
+}
diff --git a/src/lib/utils/search.ts b/src/lib/utils/search.ts
index 21fda10ea..173fcd2c9 100644
--- a/src/lib/utils/search.ts
+++ b/src/lib/utils/search.ts
@@ -71,3 +71,7 @@ export function pageNumber(page: string): number {
const number = parseInt(match[1]);
return number;
}
+
+export function getQuery(url: URL, param: string = "q"): string {
+ return url.searchParams.get(param) ?? "";
+}
diff --git a/src/lib/utils/tests/__snapshots__/viewer.test.ts.snap b/src/lib/utils/tests/__snapshots__/viewer.test.ts.snap
new file mode 100644
index 000000000..4532219eb
--- /dev/null
+++ b/src/lib/utils/tests/__snapshots__/viewer.test.ts.snap
@@ -0,0 +1,164 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`getZoomLevels 1`] = `
+[
+ [
+ "width",
+ "zoom.fitWidth",
+ ],
+ [
+ "height",
+ "zoom.fitHeight",
+ ],
+ [
+ 0.5,
+ "50%",
+ ],
+ [
+ 0.75,
+ "75%",
+ ],
+ [
+ 1,
+ "100%",
+ ],
+ [
+ 1.25,
+ "125%",
+ ],
+ [
+ 1.5,
+ "150%",
+ ],
+ [
+ 2,
+ "200%",
+ ],
+]
+`;
+
+exports[`getZoomLevels 2`] = `
+[
+ [
+ 0.5,
+ "50%",
+ ],
+ [
+ 0.75,
+ "75%",
+ ],
+ [
+ 1,
+ "100%",
+ ],
+ [
+ 1.25,
+ "125%",
+ ],
+ [
+ 1.5,
+ "150%",
+ ],
+ [
+ 2,
+ "200%",
+ ],
+]
+`;
+
+exports[`getZoomLevels 3`] = `
+[
+ [
+ "thumbnail",
+ "zoom.thumbnail",
+ ],
+ [
+ "small",
+ "zoom.small",
+ ],
+ [
+ "normal",
+ "zoom.normal",
+ ],
+ [
+ "large",
+ "zoom.large",
+ ],
+]
+`;
+
+exports[`getZoomLevels 4`] = `[]`;
+
+exports[`getZoomLevels 5`] = `
+[
+ [
+ "width",
+ "zoom.fitWidth",
+ ],
+ [
+ "height",
+ "zoom.fitHeight",
+ ],
+ [
+ 0.5,
+ "50%",
+ ],
+ [
+ 0.75,
+ "75%",
+ ],
+ [
+ 1,
+ "100%",
+ ],
+ [
+ 1.25,
+ "125%",
+ ],
+ [
+ 1.5,
+ "150%",
+ ],
+ [
+ 2,
+ "200%",
+ ],
+]
+`;
+
+exports[`getZoomLevels 6`] = `
+[
+ [
+ "width",
+ "zoom.fitWidth",
+ ],
+ [
+ "height",
+ "zoom.fitHeight",
+ ],
+ [
+ 0.5,
+ "50%",
+ ],
+ [
+ 0.75,
+ "75%",
+ ],
+ [
+ 1,
+ "100%",
+ ],
+ [
+ 1.25,
+ "125%",
+ ],
+ [
+ 1.5,
+ "150%",
+ ],
+ [
+ 2,
+ "200%",
+ ],
+]
+`;
diff --git a/src/lib/utils/tests/viewer.test.ts b/src/lib/utils/tests/viewer.test.ts
index bb0b6b5b9..dd8421dd7 100644
--- a/src/lib/utils/tests/viewer.test.ts
+++ b/src/lib/utils/tests/viewer.test.ts
@@ -1,7 +1,15 @@
-import { describe, it, expect, vi } from "vitest";
+import { describe, it, test, expect } from "vitest";
import { document } from "@/test/fixtures/documents";
import { note } from "@/test/fixtures/notes";
-import { getViewerHref } from "../viewer";
+import {
+ fitPage,
+ getViewerHref,
+ pageSizes,
+ zoomToScale,
+ zoomToSize,
+ getZoomLevels,
+ getDefaultZoom,
+} from "../viewer";
import {
canonicalUrl,
pageHashUrl,
@@ -53,3 +61,66 @@ describe("getViewerHref", () => {
);
});
});
+
+describe("pageSizes", () => {
+ // Transform page_spec value into width + height for each page
+ it("returns an empty array for an empty pageSpec value", () => {
+ expect(pageSizes(" ")).toEqual([]);
+ });
+ it("splits each pageSpec part into a new array entry", () => {
+ expect(pageSizes(";;;;;;")).toEqual(new Array(7));
+ });
+ it("checks each part for a comma-delimited value", () => {
+ expect(pageSizes("1x1:0;2x2:1-3")).toEqual([
+ [1, 1],
+ [2, 2],
+ [2, 2],
+ [2, 2],
+ ]);
+ });
+});
+
+describe("fitPage", () => {
+ it("returns sensible defaults", () => {
+ expect(fitPage(1, 1, undefined, 10)).toEqual(10);
+ expect(fitPage(1, 1, undefined, "width")).toEqual(1);
+ });
+ it("returns a scale based on the container", () => {
+ const container = { clientWidth: 1000, clientHeight: 1000 } as HTMLElement;
+ expect(fitPage(750, 1000, container, "width")).toEqual(1000 / 750);
+ expect(fitPage(750, 2000, container, "height")).toEqual(1000 / 2000);
+ });
+});
+
+test("zoomToScale", () => {
+ expect(zoomToScale("width")).toEqual("width");
+ expect(zoomToScale("height")).toEqual("height");
+ expect(zoomToScale(1.1)).toEqual(1.1);
+ expect(zoomToScale("1.2")).toEqual(1.2);
+ expect(zoomToScale(undefined)).toEqual(1);
+ expect(zoomToScale("foobar")).toEqual(1);
+});
+
+test("zoomToSize", () => {
+ expect(zoomToSize("xlarge")).toEqual("xlarge");
+ expect(zoomToSize("large")).toEqual("large");
+ expect(zoomToSize(2000)).toEqual("small");
+});
+
+test("getDefaultZoom", () => {
+ expect(getDefaultZoom("document")).toEqual("width");
+ expect(getDefaultZoom("text")).toEqual(1);
+ expect(getDefaultZoom("grid")).toEqual("small");
+ expect(getDefaultZoom("notes")).toEqual(1);
+ expect(getDefaultZoom("annotating")).toEqual("width");
+ expect(getDefaultZoom("redacting")).toEqual("width");
+});
+
+test("getZoomLevels", () => {
+ expect(getZoomLevels("document")).toMatchSnapshot();
+ expect(getZoomLevels("text")).toMatchSnapshot();
+ expect(getZoomLevels("grid")).toMatchSnapshot();
+ expect(getZoomLevels("notes")).toMatchSnapshot();
+ expect(getZoomLevels("annotating")).toMatchSnapshot();
+ expect(getZoomLevels("redacting")).toMatchSnapshot();
+});
diff --git a/src/lib/utils/viewer.ts b/src/lib/utils/viewer.ts
index 346cbc431..462dfe91a 100644
--- a/src/lib/utils/viewer.ts
+++ b/src/lib/utils/viewer.ts
@@ -1,7 +1,15 @@
-import { getContext } from "svelte";
-import type { Document, Note, ViewerMode } from "$lib/api/types";
+import type {
+ Document,
+ Note,
+ Section,
+ Sizes,
+ ViewerMode,
+ Zoom,
+} from "$lib/api/types";
+
import { canonicalUrl, pageHashUrl } from "../api/documents";
import { noteHashUrl } from "../api/notes";
+import { IMAGE_WIDTHS_MAP } from "@/config/config.js";
interface ViewerHrefOptions {
document?: Document;
@@ -11,11 +19,6 @@ interface ViewerHrefOptions {
embed?: boolean;
}
-export function isEmbedded(): boolean {
- // are we embedded?
- return getContext("embed") ?? false;
-}
-
export function getViewerHref(options: ViewerHrefOptions = {}) {
const { document, page, note, mode = "document", embed = false } = options;
let hash = "";
@@ -36,3 +39,166 @@ export function getViewerHref(options: ViewerHrefOptions = {}) {
return href;
}
}
+
+/**
+ * Return a numeric scale based on intrinsic page size and container size
+ * @param width Original document width
+ * @param height Original document height
+ * @param container
+ * @param scale
+ */
+export function fitPage(
+ width: number,
+ height: number,
+ container: HTMLElement,
+ scale: number | "width" | "height",
+): number {
+ if (typeof scale === "number") return scale;
+ if (!container) return 1;
+
+ // const [x1, y1, width, height] = page.view;
+ const { clientWidth, clientHeight } = container;
+
+ return scale === "width" ? clientWidth / width : clientHeight / height;
+}
+
+/**
+ * Parse page_spec into width and height of each page
+ *
+ * @param pageSpec A string encoding page dimensions in a compact format
+ * @returns an array of [width, height] tuples
+ */
+export function pageSizes(pageSpec: string): [width: number, height: number][] {
+ // Handle empty page spec
+ if (pageSpec.trim().length == 0) return [];
+
+ const parts = pageSpec.split(";");
+ return parts.reduce((sizes, part) => {
+ const [size, range] = part?.split(":");
+ const [width, height] = size?.split("x").map(parseFloat);
+
+ range?.split(",").forEach((rangePart) => {
+ if (rangePart.includes("-")) {
+ const [start, end] = rangePart.split("-").map((x) => parseInt(x, 10));
+ for (let page = start; page <= end; page++) {
+ sizes[page] = [width, height];
+ }
+ } else {
+ const page = parseInt(rangePart, 10);
+ sizes[page] = [width, height];
+ }
+ });
+
+ return sizes;
+ }, Array(parts.length));
+}
+
+/**
+ * Index notes by page
+ */
+export function getNotes(document: Document): Record {
+ return (
+ document.notes?.reduce>((m, note) => {
+ if (!m[note.page_number]) {
+ m[note.page_number] = [];
+ }
+ m[note.page_number].push(note);
+ return m;
+ }, {}) ?? {}
+ );
+}
+
+/**
+ * Index sections by page
+ */
+export function getSections(document: Document): Record {
+ return (
+ document.sections?.reduce((m, section) => {
+ m[section.page_number] = section;
+ return m;
+ }, {}) ?? {}
+ );
+}
+
+// for typescript
+export function zoomToScale(zoom: any): number | "width" | "height" {
+ if (zoom === "width" || zoom === "height") {
+ return zoom;
+ }
+
+ return +zoom || 1;
+}
+
+export function zoomToSize(zoom: any): Sizes {
+ if (IMAGE_WIDTHS_MAP.has(zoom)) {
+ return zoom;
+ }
+
+ return "small";
+}
+
+/**
+ * Generate a default zoom, based on mode
+ * @param mode
+ */
+export function getDefaultZoom(mode: ViewerMode): Zoom {
+ switch (mode) {
+ case "document":
+ return "width";
+
+ case "annotating":
+ return "width";
+
+ case "redacting":
+ return "width";
+
+ case "grid":
+ return "small";
+
+ default:
+ return 1;
+ }
+}
+
+/**
+ * Generate zoom levels based on mode, since each zooms in a slightly different way
+ */
+export function getZoomLevels(mode: ViewerMode): [string | number, string][] {
+ switch (mode) {
+ case "document":
+ case "annotating":
+ case "redacting":
+ return [
+ ["width", "zoom.fitWidth"],
+ ["height", "zoom.fitHeight"],
+ [0.5, "50%"],
+ [0.75, "75%"],
+ [1, "100%"],
+ [1.25, "125%"],
+ [1.5, "150%"],
+ [2, "200%"],
+ ];
+
+ case "text":
+ return [
+ [0.5, "50%"],
+ [0.75, "75%"],
+ [1, "100%"],
+ [1.25, "125%"],
+ [1.5, "150%"],
+ [2, "200%"],
+ ];
+
+ case "grid":
+ return [
+ ["thumbnail", "zoom.thumbnail"],
+ ["small", "zoom.small"],
+ ["normal", "zoom.normal"],
+ ["large", "zoom.large"],
+ ];
+
+ default:
+ // notes don't zoom
+ return [];
+ }
+}
diff --git a/src/routes/(app)/documents/[id]-[slug]/+layout.svelte b/src/routes/(app)/documents/[id]-[slug]/+layout.svelte
deleted file mode 100644
index 8023ce0c6..000000000
--- a/src/routes/(app)/documents/[id]-[slug]/+layout.svelte
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
- {#if document.noindex || document.admin_noindex}
-
- {/if}
- {document.title} | DocumentCloud
-
- {#if document.description?.trim().length > 0}
-
-
- {/if}
-
-
-
-
-
-
-
-
diff --git a/src/routes/(app)/documents/[id]-[slug]/+layout.ts b/src/routes/(app)/documents/[id]-[slug]/+layout.ts
deleted file mode 100644
index ef00c7c0b..000000000
--- a/src/routes/(app)/documents/[id]-[slug]/+layout.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Load a document for the document viewer.
- * We do this in a layout module because sub-routes can use the same
- * document without loading it again.
- */
-import type { Document, ViewerMode, ReadMode } from "@/lib/api/types";
-
-import { redirect, error } from "@sveltejs/kit";
-
-import * as documents from "$lib/api/documents";
-import { getPinnedAddons } from "$lib/api/addons";
-import { breadcrumbTrail } from "$lib/utils/index";
-
-/** @type {import('./$types').PageLoad} */
-export async function load({ fetch, params, parent, depends, url }) {
- const { data: document, error: err } = await documents.get(+params.id, fetch);
-
- if (err) {
- error(err.status, err.message);
- }
-
- if (!document) {
- error(404, "Document not found");
- }
-
- const canonical = new URL(document.canonical_url);
- if (document.slug !== params.slug) {
- redirect(302, canonical.pathname);
- }
-
- let mode: ViewerMode =
- (url.searchParams.get("mode") as ViewerMode) ?? "document";
-
- if (!documents.MODES.has(mode)) {
- mode = documents.MODES[0];
- }
-
- if (!document.edit_access && !documents.READING_MODES.has(mode as ReadMode)) {
- return redirect(302, canonical);
- }
-
- let action = url.searchParams.get("action");
-
- const breadcrumbs = await breadcrumbTrail(parent, [
- { href: canonical.pathname, title: document.title },
- ]);
-
- // stream this
- const pinnedAddons = getPinnedAddons(fetch);
-
- depends(`document:${document.id}`);
-
- return {
- document,
- mode,
- action,
- pinnedAddons,
- breadcrumbs,
- };
-}
diff --git a/src/routes/(app)/documents/[id]-[slug]/+page.svelte b/src/routes/(app)/documents/[id]-[slug]/+page.svelte
index a2f500ff9..863b19e88 100644
--- a/src/routes/(app)/documents/[id]-[slug]/+page.svelte
+++ b/src/routes/(app)/documents/[id]-[slug]/+page.svelte
@@ -1,80 +1,58 @@
-
-
- {#if shouldPreload($currentMode)}
-
+
+
+
+ {#if document.noindex || document.admin_noindex}
+
+ {/if}
+ {document.title} | DocumentCloud
+
+ {#if document.description?.trim().length > 0}
+
+
{/if}
+
+
+
+
+
-
+
+
+
diff --git a/src/routes/(app)/documents/[id]-[slug]/+page.ts b/src/routes/(app)/documents/[id]-[slug]/+page.ts
index 1e93f5db9..0f241aa17 100644
--- a/src/routes/(app)/documents/[id]-[slug]/+page.ts
+++ b/src/routes/(app)/documents/[id]-[slug]/+page.ts
@@ -1,25 +1,57 @@
-import type { DocumentText } from "$lib/api/types.js";
+/**
+ * Load a document for the document viewer.
+ * We do this in a layout module because sub-routes can use the same
+ * document without loading it again.
+ */
+import type { ReadMode } from "@/lib/api/types";
+
+import { redirect } from "@sveltejs/kit";
import * as documents from "$lib/api/documents";
+import { getPinnedAddons } from "$lib/api/addons";
+import { breadcrumbTrail } from "$lib/utils/index";
-export async function load({ fetch, parent, url, data, depends }) {
- const query = url.searchParams.get("q") ?? "";
+import loadDocument from "$lib/load/document";
+import { getQuery } from "$lib/utils/search";
- const { document, mode } = await parent();
+/** @type {import('./$types').PageLoad} */
+export async function load({ fetch, params, parent, depends, url }) {
+ let { document, text, asset_url, mode } = await loadDocument({
+ fetch,
+ params,
+ url,
+ });
- // load text
- let text = await documents.text(document, fetch);
+ depends(`document:${document.id}`);
- const asset_url = await documents.assetUrl(document, fetch);
+ const canonical = new URL(document.canonical_url);
+ if (document.slug !== params.slug) {
+ redirect(302, canonical.pathname);
+ }
- // so we can reload when we reprocess
- depends(`document:${document.id}`);
+ if (!document.edit_access && !documents.READING_MODES.has(mode as ReadMode)) {
+ return redirect(302, canonical);
+ }
+
+ let action = url.searchParams.get("action");
+
+ const breadcrumbs = await breadcrumbTrail(parent, [
+ { href: canonical.pathname, title: document.title },
+ ]);
+
+ // stream this
+ const pinnedAddons = getPinnedAddons(fetch);
+
+ const query = getQuery(url);
return {
- ...(data ?? {}), // include csrf_token
+ document,
+ mode,
+ text,
asset_url,
+ action,
+ pinnedAddons,
+ breadcrumbs,
query,
- text,
- mode,
};
}
diff --git a/src/routes/embed/documents/[id]-[slug]/+layout.svelte b/src/routes/embed/documents/[id]-[slug]/+layout.svelte
deleted file mode 100644
index fb47215f4..000000000
--- a/src/routes/embed/documents/[id]-[slug]/+layout.svelte
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- {document.title} | DocumentCloud
-
-
-
-
- {#if document.noindex || document.admin_noindex}
-
- {/if}
-
-
-
diff --git a/src/routes/embed/documents/[id]-[slug]/+layout.ts b/src/routes/embed/documents/[id]-[slug]/+layout.ts
deleted file mode 100644
index e77984846..000000000
--- a/src/routes/embed/documents/[id]-[slug]/+layout.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// DUPLICATED FROM /documents/[id]-[slug]/
-// TODO: CONSOLIDATE VIEWER LOADING LOGIC
-
-/**
- * Load a document for the document viewer.
- * We do this in a layout module because sub-routes can use the same
- * document without loading it again.
- */
-
-import { error, redirect } from "@sveltejs/kit";
-
-import * as documents from "$lib/api/documents";
-import type { ViewerMode, ReadMode } from "$lib/api/types";
-import { getEmbedSettings, type EmbedSettings } from "$lib/utils/embed.js";
-
-/** @type {import('./$types').PageLoad} */
-export async function load({ fetch, url, params, depends }) {
- const { data: document, error: err } = await documents.get(+params.id, fetch);
-
- if (err) {
- return error(err.status, err.message);
- }
-
- if (!document) {
- return error(404, "Document not found");
- }
-
- depends(`document:${document.id}`);
-
- let mode: ViewerMode =
- (url.searchParams.get("mode") as ViewerMode) ?? "document";
-
- if (!documents.MODES.has(mode)) {
- mode = documents.MODES[0];
- }
-
- // embeds are only readable
- // not sure if there's a better way to lie to typescript here
- if (!documents.READING_MODES.has(mode as ReadMode)) {
- return redirect(302, url.pathname);
- }
-
- let settings: Partial = getEmbedSettings(url.searchParams);
-
- return {
- document,
- mode,
- settings,
- };
-}
diff --git a/src/routes/embed/documents/[id]-[slug]/+page.svelte b/src/routes/embed/documents/[id]-[slug]/+page.svelte
index cd59f8721..9a7931ca7 100644
--- a/src/routes/embed/documents/[id]-[slug]/+page.svelte
+++ b/src/routes/embed/documents/[id]-[slug]/+page.svelte
@@ -1,86 +1,41 @@
-
-
+
-
- {#if shouldPreload($currentMode)}
-
+ {document.title} | DocumentCloud
+
+
+
+
+ {#if document.noindex || document.admin_noindex}
+
{/if}
-
-
-
+
+
+
+
+
diff --git a/src/routes/embed/documents/[id]-[slug]/+page.ts b/src/routes/embed/documents/[id]-[slug]/+page.ts
index c1d615144..c3ddfe22d 100644
--- a/src/routes/embed/documents/[id]-[slug]/+page.ts
+++ b/src/routes/embed/documents/[id]-[slug]/+page.ts
@@ -1,21 +1,38 @@
-import type { DocumentText, ViewerMode } from "@/lib/api/types";
-// load data for a single page embed
-import * as documents from "@/lib/api/documents";
-import * as notesApi from "$lib/api/notes";
+import type { ReadMode } from "$lib/api/types";
+
+import { redirect } from "@sveltejs/kit";
+
+import { getEmbedSettings, type EmbedSettings } from "$lib/utils/embed.js";
+import { getQuery } from "$lib/utils/search.js";
+import loadDocument from "$lib/load/document";
+import * as documents from "$lib/api/documents";
/** @type {import('./$types').PageLoad} */
-export async function load({ parent, fetch, url }) {
- const { document } = await parent();
+export async function load({ fetch, url, params, depends }) {
+ let { document, text, asset_url, mode } = await loadDocument({
+ fetch,
+ url,
+ params,
+ });
+
+ depends(`document:${document.id}`);
- const query = url.searchParams.get("q") ?? "";
+ // embeds are only readable
+ // not sure if there's a better way to lie to typescript here
+ if (!documents.READING_MODES.has(mode as ReadMode)) {
+ return redirect(302, url.pathname);
+ }
- // load text
- const text = await documents.text(document, fetch);
- const asset_url = await documents.assetUrl(document, fetch);
+ let settings: Partial = getEmbedSettings(url.searchParams);
+
+ const query = getQuery(url);
return {
+ document,
+ mode,
+ text,
asset_url,
+ settings,
query,
- text,
};
}
diff --git a/src/service-worker.ts b/src/service-worker.ts
index aa169f42a..3c6050db9 100644
--- a/src/service-worker.ts
+++ b/src/service-worker.ts
@@ -37,8 +37,9 @@ sw.addEventListener("activate", (event) => {
});
sw.addEventListener("fetch", (event) => {
- // ignore POST requests etc
- if (event.request.method !== "GET") return;
+ // ignore POST requests and browser extensions
+ if (event.request.method !== "GET" || !event.request.url.startsWith("http"))
+ return;
async function respond() {
const url = new URL(event.request.url);
diff --git a/src/style/kit.css b/src/style/kit.css
index 26b581f00..9fd99bcef 100644
--- a/src/style/kit.css
+++ b/src/style/kit.css
@@ -91,14 +91,15 @@ Updated variables and styles for new SvelteKit routes
--shadow-3: 0 2px 16px 2px rgba(30 48 56 / 0.075);
/* Z-Index Layers */
- --z-toolbar: 2;
+ --z-toolbar: 5;
+ --z-note: 2;
--z-navigation: 7;
--z-drawer: 9;
--z-modal: 10;
--z-dropdownBackdrop: 11;
--z-dropdown: 12;
--z-toast: 20;
- --z-tooltip: 21;
+ --z-tooltip: 2;
}
html,
@@ -247,4 +248,4 @@ hr.divider {
& svg {
display: block;
}
-}
\ No newline at end of file
+}