diff --git a/.gitignore b/.gitignore index 485647b..91892a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ dist/ .history/ docs/plans/ +.env \ No newline at end of file diff --git a/docs-site/public/examples/vllm/deployment.html b/docs-site/public/examples/vllm/deployment.html index dee7142..8d5e8b6 100644 --- a/docs-site/public/examples/vllm/deployment.html +++ b/docs-site/public/examples/vllm/deployment.html @@ -849,6 +849,7 @@ } const expandIcon = ``; const collapseIcon = ``; + const fullscreenTarget = this.getStoryShell() ?? this.canvasWrap ?? this.doc.documentElement; const syncIcon = () => { const isFs = !!this.doc.fullscreenElement; btn.innerHTML = isFs ? collapseIcon : expandIcon; @@ -861,12 +862,15 @@ btn.style.justifyContent = "center"; btn.addEventListener("click", () => { if (this.doc.fullscreenElement) { - this.doc.exitFullscreen(); + void this.doc.exitFullscreen(); } else { - this.doc.documentElement.requestFullscreen(); + void fullscreenTarget.requestFullscreen?.(); } }); - this.doc.addEventListener("fullscreenchange", syncIcon); + this.doc.addEventListener("fullscreenchange", () => { + syncIcon(); + this.onResize?.(); + }); } destroy() { if (this.onResize) window.removeEventListener("resize", this.onResize); diff --git a/examples/vLLM/deployment.html b/examples/vLLM/deployment.html index dee7142..8d5e8b6 100644 --- a/examples/vLLM/deployment.html +++ b/examples/vLLM/deployment.html @@ -849,6 +849,7 @@ } const expandIcon = ``; const collapseIcon = ``; + const fullscreenTarget = this.getStoryShell() ?? this.canvasWrap ?? this.doc.documentElement; const syncIcon = () => { const isFs = !!this.doc.fullscreenElement; btn.innerHTML = isFs ? collapseIcon : expandIcon; @@ -861,12 +862,15 @@ btn.style.justifyContent = "center"; btn.addEventListener("click", () => { if (this.doc.fullscreenElement) { - this.doc.exitFullscreen(); + void this.doc.exitFullscreen(); } else { - this.doc.documentElement.requestFullscreen(); + void fullscreenTarget.requestFullscreen?.(); } }); - this.doc.addEventListener("fullscreenchange", syncIcon); + this.doc.addEventListener("fullscreenchange", () => { + syncIcon(); + this.onResize?.(); + }); } destroy() { if (this.onResize) window.removeEventListener("resize", this.onResize); diff --git a/package-lock.json b/package-lock.json index eb61f9b..f12e02e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@biolytics.ai/diascope", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@biolytics.ai/diascope", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "commander": "^12.0.0", "js-yaml": "^4.1.0", diff --git a/package.json b/package.json index fdebc33..80c6f07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@biolytics.ai/diascope", - "version": "0.1.0", + "version": "0.1.1", "description": "Turn D2 diagrams into narrated interactive stories", "homepage": "https://diascope.biolytics.ai", "repository": { diff --git a/src/viewer/viewer.ts b/src/viewer/viewer.ts index c195f14..d8273a5 100644 --- a/src/viewer/viewer.ts +++ b/src/viewer/viewer.ts @@ -364,6 +364,10 @@ export class DiaScopeViewer { } const expandIcon = ``; const collapseIcon = ``; + const fullscreenTarget = + (this.getStoryShell() ?? this.canvasWrap ?? this.doc.documentElement) as (Element & { + requestFullscreen?: () => Promise; + }); const syncIcon = () => { const isFs = !!this.doc.fullscreenElement; btn!.innerHTML = isFs ? collapseIcon : expandIcon; @@ -376,12 +380,15 @@ export class DiaScopeViewer { btn.style.justifyContent = "center"; btn.addEventListener("click", () => { if (this.doc.fullscreenElement) { - this.doc.exitFullscreen(); + void this.doc.exitFullscreen(); } else { - this.doc.documentElement.requestFullscreen(); + void fullscreenTarget.requestFullscreen?.(); } }); - this.doc.addEventListener("fullscreenchange", syncIcon); + this.doc.addEventListener("fullscreenchange", () => { + syncIcon(); + this.onResize?.(); + }); } destroy(): void { diff --git a/tests/viewer.test.ts b/tests/viewer.test.ts new file mode 100644 index 0000000..7f22ffb --- /dev/null +++ b/tests/viewer.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it, vi } from "vitest"; +import { DiaScopeViewer } from "../src/viewer/viewer.js"; + +class FakeElement { + readonly style: Record = {}; + readonly children: FakeElement[] = []; + readonly listeners = new Map void>>(); + readonly attributes = new Map(); + innerHTML = ""; + title = ""; + id = ""; + requestFullscreen = vi.fn(() => Promise.resolve()); + + constructor(private readonly doc: FakeDocument, id = "") { + this.id = id; + if (id) this.attributes.set("id", id); + } + + setAttribute(name: string, value: string): void { + this.attributes.set(name, value); + if (name === "id") { + this.id = value; + this.doc.register(this); + } + } + + getAttribute(name: string): string | null { + return this.attributes.get(name) ?? null; + } + + appendChild(child: FakeElement): void { + this.children.push(child); + if (child.id) this.doc.register(child); + } + + addEventListener(type: string, listener: () => void): void { + const current = this.listeners.get(type) ?? []; + current.push(listener); + this.listeners.set(type, current); + } + + click(): void { + for (const listener of this.listeners.get("click") ?? []) listener(); + } +} + +class FakeDocument { + readonly elements = new Map(); + readonly listeners = new Map void>>(); + readonly documentElement = new FakeElement(this, "document-element"); + fullscreenElement: FakeElement | null = null; + exitFullscreen = vi.fn(() => { + this.fullscreenElement = null; + this.dispatch("fullscreenchange"); + return Promise.resolve(); + }); + + register(element: FakeElement): FakeElement { + if (element.id) this.elements.set(element.id, element); + return element; + } + + createElement(): FakeElement { + return new FakeElement(this); + } + + getElementById(id: string): FakeElement | null { + return this.elements.get(id) ?? null; + } + + querySelector(selector: string): FakeElement | null { + if (!selector.startsWith("#")) return null; + return this.getElementById(selector.slice(1)); + } + + addEventListener(type: string, listener: () => void): void { + const current = this.listeners.get(type) ?? []; + current.push(listener); + this.listeners.set(type, current); + } + + dispatch(type: string): void { + for (const listener of this.listeners.get(type) ?? []) listener(); + } +} + +describe("DiaScopeViewer expand button", () => { + it("fullscreens the viewer shell instead of the whole document when embedded", () => { + const doc = new FakeDocument(); + const canvasWrap = doc.register(new FakeElement(doc, "canvas-wrap")); + const storyShell = doc.register(new FakeElement(doc, "story-shell")); + const resizeSpy = vi.fn(); + + storyShell.requestFullscreen.mockImplementation(() => { + doc.fullscreenElement = storyShell; + doc.dispatch("fullscreenchange"); + return Promise.resolve(); + }); + + doc.documentElement.requestFullscreen.mockImplementation(() => { + doc.fullscreenElement = doc.documentElement; + doc.dispatch("fullscreenchange"); + return Promise.resolve(); + }); + + const viewer = new DiaScopeViewer({ + document: doc as unknown as Document, + svgPanZoom: (() => null) as never, + }); + viewer.canvasWrap = canvasWrap as unknown as HTMLElement; + viewer.onResize = resizeSpy; + + viewer.setupExpandButton(); + doc.getElementById("btn-expand")?.click(); + + expect(storyShell.requestFullscreen).toHaveBeenCalledTimes(1); + expect(doc.documentElement.requestFullscreen).not.toHaveBeenCalled(); + expect(resizeSpy).toHaveBeenCalledTimes(1); + }); +});