Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve search params when navigating within the viewer #985

Merged
merged 1 commit into from
Dec 16, 2024
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 changes: 7 additions & 1 deletion src/lib/components/common/Tab.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<!-- @component
A tab component for navigating within a page.
Note that links will preserve querystrings where possible.
-->
<script lang="ts">
import { qs } from "$lib/utils/navigation";

export let active = false;
export let disabled = false;
export let href: string = "";
</script>

<div class="tab" role="tab" class:active class:disabled>
{#if href}
<a {href} class:disabled>
<a {href} class:disabled use:qs>
<slot />
</a>
{:else}
Expand Down
24 changes: 13 additions & 11 deletions src/lib/components/viewer/ReadingToolbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,6 @@ Assumes it's a child of a ViewerContext
const mode = getCurrentMode();
const embed = isEmbedded();

$: document = $documentStore;
$: canWrite = !embed && document.edit_access;
$: BREAKPOINTS = {
READ_MENU: width > remToPx(52),
WRITE_MENU: width < remToPx(37),
SEARCH_MENU: width < remToPx(24),
};

const readModeTabs: Map<ReadMode, string> = new Map([
["document", $_("mode.document")],
["text", $_("mode.text")],
Expand Down Expand Up @@ -83,6 +75,18 @@ Assumes it's a child of a ViewerContext
redacting: EyeClosed16,
search: Search16,
};

$: document = $documentStore;
$: canWrite = !embed && document.edit_access;
$: BREAKPOINTS = {
READ_MENU: width > remToPx(52),
WRITE_MENU: width < remToPx(37),
SEARCH_MENU: width < remToPx(24),
};

$: current = Array.from(readModeDropdownItems ?? []).find(
([value]) => value === $mode,
)?.[1];
</script>

<PageToolbar bind:width>
Expand All @@ -103,9 +107,7 @@ Assumes it's a child of a ViewerContext
<Dropdown position="bottom-start">
<SidebarItem slot="anchor">
<svelte:component this={icons[$mode]} slot="start" />
{Array.from(readModeDropdownItems ?? []).find(
([value]) => value === $mode,
)?.[1]}
{current}
<ChevronDown12 slot="end" />
</SidebarItem>
<Menu slot="default" let:close>
Expand Down
18 changes: 11 additions & 7 deletions src/lib/components/viewer/Search.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,32 @@ Assumes it's a child of a ViewerContext
-->

<script lang="ts">
import type { APIResponse, Highlights } from "@/lib/api/types";
import type { APIResponse, Highlights } from "$lib/api/types";

import { page } from "$app/stores";

import { getContext } from "svelte";
import { _ } from "svelte-i18n";
import { Hourglass24, Search24 } from "svelte-octicons";

import { page } from "$app/stores";

import Empty from "../common/Empty.svelte";
import Error from "../common/Error.svelte";
import Highlight from "../common/Highlight.svelte";

import { getDocument } from "./ViewerContext.svelte";
import { getQuery, highlight, pageNumber } from "$lib/utils/search";
import { getViewerHref } from "$lib/utils/viewer";
import { qs } from "$lib/utils/navigation";
import { searchWithin } from "$lib/api/documents";

const document = getDocument();
const embed: boolean = getContext("embed");

let search: Promise<[number, string[]][]>;

$: query = getQuery($page.url, "q");
$: search = searchWithin($document.id, query).then(formatResults);

// Format page numbers, highlight search results, and remove invalid pages
function formatResults(results: APIResponse<Highlights>) {
if (results.error) throw new TypeError(results.error.message);
Expand All @@ -35,9 +42,6 @@ Assumes it's a child of a ViewerContext
.filter(([page]) => !isNaN(page));
}

$: query = getQuery($page.url, "q");
$: search = searchWithin($document.id, query).then(formatResults);

function countResults(results: [number, string[]][]) {
return results.reduce((acc, [, segments]) => acc + segments.length, 0);
}
Expand Down Expand Up @@ -69,7 +73,7 @@ Assumes it's a child of a ViewerContext
query,
})}

<a {href} class="card">
<a use:qs {href} class="card">
<Highlight
title="{$_('documents.pageAbbrev')} {pageNumber}"
segments={resultsList}
Expand Down
22 changes: 22 additions & 0 deletions src/lib/utils/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Action } from "svelte/action";

type Breadcrumb = { href?: string; title: string };
type Parent = () => Promise<{ breadcrumbs?: Array<Breadcrumb> }>;

Expand All @@ -11,3 +13,23 @@ export async function breadcrumbTrail(
const { breadcrumbs: trail = [] } = await parent();
return trail.concat(crumbs);
}

/**
* Make a link preserve existing query params and merge in new ones
*
* @type {Action}
* @param node
*/
export function qs(node: HTMLAnchorElement) {
if (typeof window === "undefined") return;

const href = new URL(node.href);
const params = new URLSearchParams(window.location.search);

for (const [k, v] of href.searchParams) {
params.set(k, v);
}

href.search = params.toString();
node.href = href.toString();
}
41 changes: 40 additions & 1 deletion src/lib/utils/tests/navigation.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
/**
* @vitest-environment jsdom
* @vitest-environment-options { "url": "https://www.dev.documentcloud.org/documents/20000065-creating-adaptable-skills-a-nonlinear-pedagogy-approach-to-mental-imagery/?mode=search&embed=1&q=pedagogy&title=0" }
*/

import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import { breadcrumbTrail } from "../navigation";
import { breadcrumbTrail, qs } from "../navigation";

describe("breadcrumbTrail", () => {
it("returns an empty array as a base case", async () => {
const parent = vi.fn().mockResolvedValue({});
expect(await breadcrumbTrail(parent)).toEqual([]);
});

it("returns the parent's breadcrumb trail, if it exists", async () => {
const parentTrail = [{ href: "/first", title: "First Level" }];
const parent = vi.fn().mockResolvedValue({ breadcrumbs: parentTrail });
expect(await breadcrumbTrail(parent)).toEqual(parentTrail);
});

it("concats the provided trail onto the parent's trail", async () => {
const parentTrail = [{ href: "/first", title: "First Level" }];
const childTrail = [{ href: "/second", title: "Second Level" }];
Expand All @@ -21,3 +28,35 @@ describe("breadcrumbTrail", () => {
]);
});
});

describe("querystring links", () => {
afterEach(() => {
vi.resetAllMocks();
});

it("adds new query params to existing URL params", () => {
const links = [
"https://www.dev.documentcloud.org/documents/20000065-creating-adaptable-skills-a-nonlinear-pedagogy-approach-to-mental-imagery/?mode=document#document/p1",
"https://www.dev.documentcloud.org/documents/20000065-creating-adaptable-skills-a-nonlinear-pedagogy-approach-to-mental-imagery/?mode=notes",
"https://www.dev.documentcloud.org/documents/20000065-creating-adaptable-skills-a-nonlinear-pedagogy-approach-to-mental-imagery/?mode=document",
];

const fixed = [
"?mode=document&embed=1&q=pedagogy&title=0",
"?mode=notes&embed=1&q=pedagogy&title=0",
"?mode=document&embed=1&q=pedagogy&title=0",
];

links.forEach((href, i) => {
// create the link
let a = document.createElement("a");
a.href = href;
a.textContent = "test";

// fix the link
qs(a);

expect(new URL(a.href).search).toEqual(fixed[i]);
});
});
});
Loading