Skip to content

Commit 803408d

Browse files
committed
[jer/preview-like-prod] Get 'article-all' Article preview working
1 parent 18d878d commit 803408d

File tree

4 files changed

+129
-73
lines changed

4 files changed

+129
-73
lines changed

packages/perseus-editor/src/__docs__/article-editor.stories.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {ApiOptions} from "@khanacademy/perseus";
22
import {View} from "@khanacademy/wonder-blocks-core";
33
import * as React from "react";
4-
import {useRef, useState, useMemo} from "react";
4+
import {useState, useMemo} from "react";
55

66
import {testDependenciesV2} from "../../../../testing/test-dependencies";
77
import {comprehensiveQuestion} from "../__testdata__/all-widgets.testdata";
@@ -17,15 +17,18 @@ export default {
1717
title: "Editors/ArticleEditor",
1818
};
1919

20+
function getStorybookPreviewUrl() {
21+
return `${window.location.origin}/iframe.html?id=dev-support-preview--default&viewMode=story`;
22+
}
23+
2024
export const Demo = (): React.ReactElement => {
2125
// Start with one empty section so the editor and preview are visible
2226
const [article, setArticle] = useState([
2327
{content: "", widgets: {}, images: {}},
2428
]);
25-
const articleEditorRef = useRef();
2629

2730
const storybookPreviewUrl = useMemo(() => {
28-
return `${window.location.origin}/iframe.html?id=dev-support-preview--default&viewMode=story`;
31+
return getStorybookPreviewUrl();
2932
}, []);
3033

3134
return (
@@ -39,21 +42,19 @@ export const Demo = (): React.ReactElement => {
3942
setArticle(value.json);
4043
}}
4144
previewURL={storybookPreviewUrl}
42-
ref={articleEditorRef as any}
4345
/>
4446
</View>
4547
);
4648
};
4749

4850
export const WithEditingDisabled = (): React.ReactElement => {
49-
const articleEditorRef = useRef();
5051
const disabledApiOptions = {
5152
...ApiOptions.defaults,
5253
editingDisabled: true,
5354
};
5455

5556
const storybookPreviewUrl = useMemo(() => {
56-
return `${window.location.origin}/iframe.html?id=dev-support-preview--default&viewMode=story`;
57+
return getStorybookPreviewUrl();
5758
}, []);
5859

5960
return (
@@ -64,7 +65,24 @@ export const WithEditingDisabled = (): React.ReactElement => {
6465
json={[comprehensiveQuestion]}
6566
onChange={() => {}}
6667
previewURL={storybookPreviewUrl}
67-
ref={articleEditorRef as any}
68+
/>
69+
);
70+
};
71+
72+
export const PreviewMode = (): React.ReactElement => {
73+
const storybookPreviewUrl = useMemo(() => {
74+
return getStorybookPreviewUrl();
75+
}, []);
76+
77+
return (
78+
<ArticleEditor
79+
dependencies={testDependenciesV2}
80+
apiOptions={ApiOptions.defaults}
81+
mode="preview"
82+
imageUploader={() => {}}
83+
json={[comprehensiveQuestion, comprehensiveQuestion]}
84+
onChange={() => {}}
85+
previewURL={storybookPreviewUrl}
6886
/>
6987
);
7088
};

packages/perseus-editor/src/article-editor.tsx

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* multiple (Renderer) sections concatenated together.
55
*/
66

7-
import {components, ApiOptions, Dependencies} from "@khanacademy/perseus";
7+
import {components, ApiOptions, Dependencies, Log} from "@khanacademy/perseus";
88
import {Errors, PerseusError} from "@khanacademy/perseus-core";
99
import Button from "@khanacademy/wonder-blocks-button";
1010
import arrowCircleDownIcon from "@phosphor-icons/core/bold/arrow-circle-down-bold.svg";
@@ -20,6 +20,7 @@ import SectionControlButton from "./components/section-control-button";
2020
import Editor from "./editor";
2121
import IframeContentRenderer from "./iframe-content-renderer";
2222

23+
import type {IframeContentRendererRef} from "./iframe-content-renderer";
2324
import type {
2425
APIOptions,
2526
Changeable,
@@ -58,6 +59,7 @@ type Props = DefaultProps & {
5859
type State = {
5960
highlightLint: boolean;
6061
};
62+
6163
export default class ArticleEditor extends React.Component<Props, State> {
6264
static defaultProps: DefaultProps = {
6365
contentPaths: [],
@@ -72,32 +74,57 @@ export default class ArticleEditor extends React.Component<Props, State> {
7274
highlightLint: true,
7375
};
7476

77+
// Store refs for preview iframes (keyed by section index or "all")
78+
private frameRefs: Record<string, IframeContentRendererRef | null> = {};
79+
7580
componentDidMount() {
76-
this._updatePreviewFrames();
81+
// Defer updatePreviewFrames to ensure refs are set
82+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
83+
// eslint-disable-next-line no-restricted-syntax
84+
setTimeout(() => {
85+
this._updatePreviewFrames();
86+
}, 0);
7787
}
7888

7989
componentDidUpdate() {
80-
this._updatePreviewFrames();
90+
// Defer updatePreviewFrames to allow for child renders
91+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
92+
// eslint-disable-next-line no-restricted-syntax
93+
setTimeout(() => {
94+
this._updatePreviewFrames();
95+
}, 0);
8196
}
8297

8398
_updatePreviewFrames() {
8499
if (this.props.mode === "preview") {
85-
// eslint-disable-next-line react/no-string-refs
86-
// @ts-expect-error - TS2339 - Property 'sendNewData' does not exist on type 'ReactInstance'.
87-
this.refs["frame-all"].sendNewData({
88-
type: "article-all",
89-
data: this._sections().map((section, i) => {
90-
return this._apiOptionsForSection(section, i);
91-
}),
100+
const frameAll = this.frameRefs["all"];
101+
Log.log("[ArticleEditor] Preview mode - frameAll ref", {
102+
hasRef: !!frameAll,
92103
});
104+
if (frameAll) {
105+
const data = this._sections().map((section, i) => {
106+
return this._apiOptionsForSection(section, i);
107+
});
108+
Log.log("[ArticleEditor] Sending article-all data", {
109+
sections: data.length,
110+
data: JSON.stringify(data),
111+
});
112+
frameAll.sendNewData({
113+
type: "article-all",
114+
data,
115+
});
116+
} else {
117+
Log.log("[ArticleEditor] No frameAll ref available yet");
118+
}
93119
} else if (this.props.mode === "edit") {
94120
this._sections().forEach((section, i) => {
95-
// eslint-disable-next-line react/no-string-refs
96-
// @ts-expect-error - TS2339 - Property 'sendNewData' does not exist on type 'ReactInstance'.
97-
this.refs["frame-" + i].sendNewData({
98-
type: "article",
99-
data: this._apiOptionsForSection(section, i),
100-
});
121+
const frame = this.frameRefs[String(i)];
122+
if (frame) {
123+
frame.sendNewData({
124+
type: "article",
125+
data: this._apiOptionsForSection(section, i),
126+
});
127+
}
101128
});
102129
}
103130
}
@@ -300,7 +327,9 @@ export default class ArticleEditor extends React.Component<Props, State> {
300327
return (
301328
<DeviceFramer deviceType={this.props.screen} nochrome={nochrome}>
302329
<IframeContentRenderer
303-
ref={"frame-" + i}
330+
ref={(node) => {
331+
this.frameRefs[String(i)] = node;
332+
}}
304333
key={this.props.screen}
305334
isMobile={isMobile}
306335
seamless={nochrome}

packages/perseus-editor/src/iframe-content-renderer.tsx

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,63 +25,62 @@ type Props = {
2525
seamless: boolean;
2626
};
2727

28-
type IframeContentRendererHandle = {
28+
export type IframeContentRendererRef = {
2929
sendNewData: (data: PreviewContent) => void;
3030
};
3131

32-
const IframeContentRenderer = React.forwardRef<
33-
IframeContentRendererHandle,
34-
Props
35-
>((props, ref) => {
36-
const containerRef = React.useRef<HTMLDivElement>(null);
37-
const iframeRef = React.useRef<HTMLIFrameElement>(null);
38-
// ID is for debugging/logging, not routing (which uses event.source)
39-
const [iframeId] = React.useState(() => String(nextIframeID++));
32+
const IframeContentRenderer = React.forwardRef<IframeContentRendererRef, Props>(
33+
(props, ref) => {
34+
const containerRef = React.useRef<HTMLDivElement>(null);
35+
const iframeRef = React.useRef<HTMLIFrameElement>(null);
36+
// ID is for debugging/logging, not routing (which uses event.source)
37+
const [iframeId] = React.useState(() => String(nextIframeID++));
4038

41-
const {sendData, height} = usePreviewHost(iframeRef);
39+
const {sendData, height} = usePreviewHost(iframeRef);
4240

43-
// Update container height based on iframe content height
44-
React.useEffect(() => {
45-
if (!containerRef.current) {
46-
return;
47-
}
41+
// Update container height based on iframe content height
42+
React.useEffect(() => {
43+
if (!containerRef.current) {
44+
return;
45+
}
4846

49-
if (!props.seamless) {
50-
containerRef.current.style.height = "100%";
51-
} else if (height !== null) {
52-
containerRef.current.style.height = `${height}px`;
53-
}
54-
}, [height, props.seamless]);
47+
if (!props.seamless) {
48+
containerRef.current.style.height = "100%";
49+
} else if (height !== null) {
50+
containerRef.current.style.height = `${height}px`;
51+
}
52+
}, [height, props.seamless]);
5553

56-
// Expose sendNewData method via ref
57-
React.useImperativeHandle(
58-
ref,
59-
() => ({
60-
sendNewData: (data: PreviewContent) => {
61-
sendData(data);
62-
},
63-
}),
64-
[sendData],
65-
);
54+
// Expose sendNewData method via ref
55+
React.useImperativeHandle(
56+
ref,
57+
() => ({
58+
sendNewData: (data: PreviewContent) => {
59+
sendData(data);
60+
},
61+
}),
62+
[sendData],
63+
);
6664

67-
return (
68-
<div ref={containerRef} style={{width: "100%", height: "100%"}}>
69-
<iframe
70-
ref={iframeRef}
71-
title={`perseus-preview-${iframeId}`}
72-
data-id={iframeId}
73-
data-mobile={props.isMobile ? "true" : "false"}
74-
// The seamless prop is the same as the "nochrome" prop that
75-
// gets passed to DeviceFramer. If it is set, then we're going
76-
// to be displaying editor previews and want to leave some room
77-
// for lint indicators in the right margin.
78-
data-lint-gutter={props.seamless ? "true" : "false"}
79-
style={{width: "100%", height: "100%"}}
80-
src={props.url}
81-
/>
82-
</div>
83-
);
84-
});
65+
return (
66+
<div ref={containerRef} style={{width: "100%", height: "100%"}}>
67+
<iframe
68+
ref={iframeRef}
69+
title={`perseus-preview-${iframeId}`}
70+
data-id={iframeId}
71+
data-mobile={props.isMobile ? "true" : "false"}
72+
// The seamless prop is the same as the "nochrome" prop that
73+
// gets passed to DeviceFramer. If it is set, then we're going
74+
// to be displaying editor previews and want to leave some room
75+
// for lint indicators in the right margin.
76+
data-lint-gutter={props.seamless ? "true" : "false"}
77+
style={{width: "100%", height: "100%"}}
78+
src={props.url}
79+
/>
80+
</div>
81+
);
82+
},
83+
);
8584

8685
IframeContentRenderer.displayName = "IframeContentRenderer";
8786

packages/perseus-editor/src/preview/use-preview-host.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ export function usePreviewHost(
8787
},
8888
} as PreviewContent;
8989
}
90+
91+
if (data.type === "article-all") {
92+
return {
93+
...data,
94+
data: data.data.map((section) => ({
95+
...section,
96+
apiOptions: sanitizeApiOptions(section.apiOptions),
97+
})),
98+
} as PreviewContent;
99+
}
90100
return data;
91101
},
92102
[],

0 commit comments

Comments
 (0)