mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): preserve deck preview pagination per file (#119)
Generated-By: looper 0.2.7 (runner=worker, agent=codex)
This commit is contained in:
parent
4e3d82cdcd
commit
85032f530c
3 changed files with 57 additions and 11 deletions
|
|
@ -22,6 +22,9 @@ import type { DeployConfigResponse, DeployProjectFileResponse, ProjectFile } fro
|
|||
import { Icon } from './Icon';
|
||||
|
||||
type TranslateFn = (key: keyof Dict, vars?: Record<string, string | number>) => string;
|
||||
type SlideState = { active: number; count: number };
|
||||
|
||||
const htmlPreviewSlideState = new Map<string, SlideState>();
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
|
|
@ -244,10 +247,13 @@ function HtmlViewer({
|
|||
const [teamSlug, setTeamSlug] = useState('');
|
||||
const [inTabPresent, setInTabPresent] = useState(false);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const previewStateKey = `${projectId}:${file.name}`;
|
||||
// Slide deck nav state: the iframe posts the active index + total count
|
||||
// back to the host every time a slide settles. Host renders prev/next
|
||||
// controls in the toolbar and reflects the count beside them.
|
||||
const [slideState, setSlideState] = useState<{ active: number; count: number } | null>(null);
|
||||
const [slideState, setSlideState] = useState<SlideState | null>(
|
||||
() => htmlPreviewSlideState.get(previewStateKey) ?? null,
|
||||
);
|
||||
const previewBodyRef = useRef<HTMLDivElement | null>(null);
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const shareRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -313,8 +319,9 @@ function HtmlViewer({
|
|||
() => (previewSource ? buildSrcdoc(previewSource, {
|
||||
deck: effectiveDeck,
|
||||
baseHref: projectRawUrl(projectId, baseDirFor(file.name)),
|
||||
initialSlideIndex: htmlPreviewSlideState.get(previewStateKey)?.active ?? 0,
|
||||
}) : ''),
|
||||
[previewSource, effectiveDeck, projectId, file.name],
|
||||
[previewSource, effectiveDeck, projectId, file.name, previewStateKey],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -322,17 +329,21 @@ function HtmlViewer({
|
|||
setSlideState(null);
|
||||
return;
|
||||
}
|
||||
setSlideState(htmlPreviewSlideState.get(previewStateKey) ?? null);
|
||||
function onMessage(ev: MessageEvent) {
|
||||
if (ev.source !== iframeRef.current?.contentWindow) return;
|
||||
const data = ev?.data as
|
||||
| { type?: string; active?: number; count?: number }
|
||||
| null;
|
||||
if (!data || data.type !== 'od:slide-state') return;
|
||||
if (typeof data.active !== 'number' || typeof data.count !== 'number') return;
|
||||
setSlideState({ active: data.active, count: data.count });
|
||||
const next = { active: data.active, count: data.count };
|
||||
htmlPreviewSlideState.set(previewStateKey, next);
|
||||
setSlideState(next);
|
||||
}
|
||||
window.addEventListener('message', onMessage);
|
||||
return () => window.removeEventListener('message', onMessage);
|
||||
}, [effectiveDeck]);
|
||||
}, [effectiveDeck, previewStateKey]);
|
||||
|
||||
function postSlide(action: 'next' | 'prev' | 'first' | 'last') {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,31 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildSrcdoc } from './srcdoc';
|
||||
|
||||
describe('buildSrcdoc deck bridge', () => {
|
||||
const deckHtml = `<!doctype html>
|
||||
<html>
|
||||
<head><title>Deck</title></head>
|
||||
<body>
|
||||
<section class="slide active">One</section>
|
||||
<section class="slide">Two</section>
|
||||
<section class="slide">Three</section>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
describe('buildSrcdoc', () => {
|
||||
it('injects an initial slide index for deck previews', () => {
|
||||
const doc = buildSrcdoc(deckHtml, { deck: true, initialSlideIndex: 2 });
|
||||
|
||||
expect(doc).toContain('var initialSlideIndex = 2;');
|
||||
expect(doc).toContain('setTimeout(restoreInitialSlide, 200)');
|
||||
expect(doc).toContain('setTimeout(restoreInitialSlide, 100)');
|
||||
});
|
||||
|
||||
it('clamps invalid initial slide indices before injecting deck bridge script', () => {
|
||||
const doc = buildSrcdoc(deckHtml, { deck: true, initialSlideIndex: -4 });
|
||||
|
||||
expect(doc).toContain('var initialSlideIndex = 0;');
|
||||
});
|
||||
|
||||
it('only uses directly mutable slide conventions for setActive support', () => {
|
||||
const srcdoc = buildSrcdoc(
|
||||
'<section class="slide">One</section><section class="slide">Two</section>',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
export function buildSrcdoc(
|
||||
html: string,
|
||||
options: { deck?: boolean; baseHref?: string } = {}
|
||||
options: { deck?: boolean; baseHref?: string; initialSlideIndex?: number } = {}
|
||||
): string {
|
||||
const head = html.trimStart().slice(0, 64).toLowerCase();
|
||||
const isFullDoc = head.startsWith("<!doctype") || head.startsWith("<html");
|
||||
|
|
@ -33,7 +33,7 @@ export function buildSrcdoc(
|
|||
const withBase = options.baseHref ? injectBaseHref(wrapped, options.baseHref) : wrapped;
|
||||
const withShim = injectSandboxShim(withBase);
|
||||
if (!options.deck) return withShim;
|
||||
return injectDeckBridge(withShim);
|
||||
return injectDeckBridge(withShim, options.initialSlideIndex);
|
||||
}
|
||||
|
||||
function injectBaseHref(doc: string, baseHref: string): string {
|
||||
|
|
@ -119,7 +119,10 @@ function injectSandboxShim(doc: string): string {
|
|||
// the scaled canvas ends up offset toward the bottom-right of any
|
||||
// preview that's smaller than 1920x1080 — exactly what users see in the
|
||||
// sandbox iframe. `place-content: center` centers the track itself.
|
||||
function injectDeckBridge(doc: string): string {
|
||||
function injectDeckBridge(doc: string, initialSlideIndex = 0): string {
|
||||
const safeInitialSlideIndex = Number.isFinite(initialSlideIndex)
|
||||
? Math.max(0, Math.floor(initialSlideIndex))
|
||||
: 0;
|
||||
const styleFix = `<style data-od-deck-fix>
|
||||
.stage, .deck-stage, .deck-shell { place-content: center !important; }
|
||||
</style>`;
|
||||
|
|
@ -130,6 +133,8 @@ function injectDeckBridge(doc: string): string {
|
|||
: styleFix + doc;
|
||||
doc = docWithStyle;
|
||||
const script = `<script>(function(){
|
||||
var initialSlideIndex = ${safeInitialSlideIndex};
|
||||
var didRestoreInitialSlide = initialSlideIndex <= 0;
|
||||
function slides(){ return document.querySelectorAll('.slide'); }
|
||||
function scroller(){
|
||||
if (document.body && document.body.scrollWidth > document.body.clientWidth + 1) return document.body;
|
||||
|
|
@ -292,6 +297,13 @@ function injectDeckBridge(doc: string): string {
|
|||
}, '*');
|
||||
} catch (e) {}
|
||||
}
|
||||
function restoreInitialSlide(){
|
||||
if (didRestoreInitialSlide) { report(); return; }
|
||||
var list = slides();
|
||||
if (!list.length) return;
|
||||
didRestoreInitialSlide = true;
|
||||
gotoIndex(initialSlideIndex);
|
||||
}
|
||||
window.addEventListener('message', function(ev){
|
||||
var data = ev && ev.data;
|
||||
if (!data || data.type !== 'od:slide') return;
|
||||
|
|
@ -311,7 +323,7 @@ function injectDeckBridge(doc: string): string {
|
|||
ownDeckButton('deck-prev', 'prev');
|
||||
ownDeckButton('deck-next', 'next');
|
||||
// Report once on load and on every scroll-end so the host stays in sync.
|
||||
window.addEventListener('load', function(){ setTimeout(report, 200); });
|
||||
window.addEventListener('load', function(){ setTimeout(restoreInitialSlide, 200); });
|
||||
document.addEventListener('scroll', function(){
|
||||
clearTimeout(window.__odReportT);
|
||||
window.__odReportT = setTimeout(report, 120);
|
||||
|
|
@ -365,7 +377,7 @@ function injectDeckBridge(doc: string): string {
|
|||
mo.observe(list[i], { attributes: true, attributeFilter: ['class', 'style', 'hidden', 'aria-hidden'] });
|
||||
}
|
||||
} catch (e) {}
|
||||
setTimeout(report, 100);
|
||||
setTimeout(restoreInitialSlide, 100);
|
||||
}
|
||||
observeSlides();
|
||||
})();</script>`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue