Skip to content

Commit d616b90

Browse files
iframe: zoom and new tab (#1493)
1 parent 6d72ca6 commit d616b90

12 files changed

Lines changed: 227 additions & 20 deletions

File tree

znai-core/src/main/java/org/testingisdocumenting/znai/extensions/html/IframeIncludePlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public PluginParamsDefinition parameters() {
5959
params.add("aspectRatio", PluginParamType.STRING, "aspect ratio for video embedding", "\"16:9\"");
6060
params.add("light", PluginParamType.OBJECT, "CSS properties override for light theme", "{ \"--color\": \"#333\" }");
6161
params.add("dark", PluginParamType.OBJECT, "CSS properties override for dark theme", "{ \"--color\": \"#eee\" }");
62+
params.add("zoomEnabled", PluginParamType.BOOLEAN, "enable full-screen zoom button in the title bar", "true");
63+
params.add("newTabEnabled", PluginParamType.BOOLEAN, "enable open in new tab button in the title bar", "true");
6264
return params;
6365
}
6466

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Add: iframe zoom and open in new tab

znai-docs/znai/visuals/iframe.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,34 @@ Use `wide` to take all the available horizontal space:
107107
wide: true
108108
}
109109

110+
# Zoom And New Tab
111+
112+
Use `zoomEnabled` to add a full-screen zoom button to the title bar.
113+
Clicking the button opens the iframe in a full-screen overlay with a close button. Press `Escape` or click the close button to exit.
114+
115+
Use `newTabEnabled` to add an open-in-new-tab button to the title bar.
116+
117+
Note: Requires `title` to be set.
118+
119+
```markdown {highlight: ["zoomEnabled", "newTabEnabled"]}
120+
:include-iframe: iframe/custom-multi-line.html {
121+
title: "parameters reference",
122+
fit: true,
123+
maxHeight: 120,
124+
zoomEnabled: true,
125+
newTabEnabled: true
126+
}
127+
```
128+
129+
:include-iframe: iframe/custom-multi-line.html {
130+
title: "parameters reference",
131+
fit: true,
132+
maxHeight: 120,
133+
zoomEnabled: true,
134+
newTabEnabled: true
135+
}
136+
137+
110138
# Embedding Video
111139

112140
Use `include-iframe` to embed media from other places. By default, aspect ratio is set to `16:9`.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<html lang="en">
2+
<head>
3+
<style>
4+
:root {
5+
--backgroundColor: #eee;
6+
--color: #222;
7+
}
8+
9+
body { margin: 0 }
10+
11+
.content {
12+
background-color: var(--backgroundColor);
13+
color: var(--color);
14+
font-size: 16px;
15+
padding: 16px;
16+
line-height: 1.6;
17+
}
18+
19+
table {
20+
border-collapse: collapse;
21+
width: 100%;
22+
margin-top: 12px;
23+
}
24+
25+
th, td {
26+
border: 1px solid #999;
27+
padding: 8px 12px;
28+
text-align: left;
29+
}
30+
31+
th {
32+
background-color: rgba(128, 128, 128, 0.15);
33+
}
34+
</style>
35+
<title>multi line content</title>
36+
</head>
37+
38+
<body>
39+
<div class="content">
40+
<p>This is an example with more content to demonstrate zoom functionality.</p>
41+
<table>
42+
<tr><th>Parameter</th><th>Type</th><th>Description</th></tr>
43+
<tr><td>src</td><td>string</td><td>URL of the content to embed</td></tr>
44+
<tr><td>fit</td><td>boolean</td><td>Auto resize iframe to fit content</td></tr>
45+
<tr><td>title</td><td>string</td><td>Title bar text</td></tr>
46+
<tr><td>wide</td><td>boolean</td><td>Take all available horizontal space</td></tr>
47+
<tr><td>zoomEnabled</td><td>boolean</td><td>Show full-screen zoom button</td></tr>
48+
<tr><td>newTabEnabled</td><td>boolean</td><td>Show open in new tab button</td></tr>
49+
</table>
50+
</div>
51+
</body>
52+
</html>

znai-reactjs/src/doc-elements/container/Container.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
.znai-container.wide .znai-container-title-wrapper {
3030
border: none;
31+
padding-right: 0;
3132
}
3233

3334
.znai-container.wide .znai-container-title {

znai-reactjs/src/doc-elements/container/Container.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface Props extends ContainerCommonProps {
3838
onCollapseToggle?(): void;
3939
additionalTitleClassNames?: string;
4040
additionalTitleContainerClassNames?: string;
41+
titleActions?: React.ReactNode;
4142
titleContainerStyle?: CSSProperties;
4243
style?: CSSProperties;
4344
onClick?(): void;
@@ -68,6 +69,7 @@ export function Container({
6869
onCollapseToggle,
6970
additionalTitleClassNames,
7071
additionalTitleContainerClassNames,
72+
titleActions,
7173
titleContainerStyle,
7274
style,
7375
onClick,
@@ -96,6 +98,7 @@ export function Container({
9698
onCollapseToggle={onCollapseToggle}
9799
additionalTitleClassNames={additionalTitleClassNames}
98100
additionalContainerClassNames={additionalTitleContainerClassNames}
101+
titleActions={titleActions}
99102
containerStyle={titleContainerStyle}
100103
/>
101104
) : null;

znai-reactjs/src/doc-elements/container/ContainerTitle.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
.znai-container-title {
2626
display: flex;
2727
align-items: center;
28+
flex: 1;
2829
color: var(--znai-snippets-title-color);
2930
padding: 8px 0 8px 16px;
3031
}
@@ -62,4 +63,26 @@
6263
.znai-container-title-anchor .znai-icon svg {
6364
width: 14px;
6465
height: 14px;
66+
}
67+
68+
.znai-container-title-actions {
69+
margin-left: auto;
70+
display: flex;
71+
align-items: center;
72+
gap: 4px;
73+
}
74+
75+
.znai-container-title-actions .znai-icon {
76+
cursor: pointer;
77+
opacity: 0.6;
78+
}
79+
80+
.znai-container-title-actions .znai-icon:hover {
81+
opacity: 1;
82+
}
83+
84+
.znai-container-title-actions .znai-icon,
85+
.znai-container-title-actions .znai-icon svg {
86+
width: 14px;
87+
height: 14px;
6588
}

znai-reactjs/src/doc-elements/container/ContainerTitle.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface Props extends ContainerTitleCommonProps {
3030
additionalContainerClassNames?: string;
3131
additionalTitleClassNames?: string;
3232
containerStyle?: React.CSSProperties;
33+
titleActions?: React.ReactNode;
3334

3435
onCollapseToggle?(): void;
3536
}
@@ -45,6 +46,7 @@ export function ContainerTitle({
4546
containerStyle,
4647
collapsed,
4748
anchorId,
49+
titleActions,
4850
onCollapseToggle,
4951
}: Props) {
5052
const collapsible = collapsed !== undefined;
@@ -77,6 +79,7 @@ export function ContainerTitle({
7779
</a>
7880
</div>
7981
)}
82+
{titleActions && <div className="znai-container-title-actions">{titleActions}</div>}
8083
</div>
8184
</div>
8285
);

znai-reactjs/src/doc-elements/iframe/Iframe.css

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,47 @@
2424
}
2525

2626
.znai-iframe-title {
27-
padding: 4px 16px;
27+
padding-top: 4px;
28+
padding-bottom: 4px;
2829
font-size: var(--znai-smaller-text-size);
30+
}
31+
32+
.znai-iframe-zoomed {
33+
display: flex;
34+
flex-direction: column;
35+
width: calc(100vw - 32px);
36+
height: calc(100vh - 32px);
37+
background: var(--znai-background-color);
38+
}
39+
40+
.znai-iframe-zoomed-header {
41+
display: flex;
42+
align-items: center;
43+
justify-content: space-between;
44+
padding: 8px 4px 8px 16px;
45+
background: var(--znai-snippets-title-background-color);
46+
color: var(--znai-snippets-title-color);
47+
font-size: var(--znai-smaller-text-size);
48+
}
49+
50+
.znai-iframe-zoomed-close {
51+
cursor: pointer;
52+
opacity: 0.6;
53+
}
54+
55+
.znai-iframe-zoomed-close:hover {
56+
opacity: 1;
57+
}
58+
59+
.znai-iframe-zoomed-close.znai-icon,
60+
.znai-iframe-zoomed-close.znai-icon svg {
61+
width: 18px;
62+
height: 18px;
63+
}
64+
65+
.znai-iframe-zoomed-content {
66+
flex: 1;
67+
border: 0;
68+
width: 100%;
69+
height: 100%;
2970
}

znai-reactjs/src/doc-elements/iframe/Iframe.tsx

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import React, { useEffect, useRef, useState } from "react";
1818
import { Container } from "../container/Container";
19+
import { Icon } from "../icons/Icon";
20+
import { zoom } from "../zoom/Zoom";
1921

2022
import "./Iframe.css";
2123

@@ -29,6 +31,8 @@ interface Props {
2931
wide?: boolean;
3032
height?: number;
3133
maxHeight?: number;
34+
zoomEnabled?: boolean;
35+
newTabEnabled?: boolean;
3236
// changes on every page regen to force iframe reload
3337
previewMarker?: string;
3438
}
@@ -44,7 +48,7 @@ export function Iframe(props: Props) {
4448
const initialIframeHeight = 14;
4549

4650
let activeElement: any = null;
47-
export function IframeFit({ src, title, wide, height, maxHeight, light, dark, previewMarker }: Props) {
51+
export function IframeFit({ src, title, wide, height, maxHeight, light, dark, zoomEnabled, newTabEnabled, previewMarker }: Props) {
4852
const containerRef = useRef<HTMLDivElement>(null);
4953
const iframeRef = useRef<HTMLIFrameElement>(null);
5054
const mutationObserverRef = useRef<MutationObserver | null>(null);
@@ -60,20 +64,9 @@ export function IframeFit({ src, title, wide, height, maxHeight, light, dark, pr
6064
iframeRef!.current!.src += "";
6165
}, [previewMarker]);
6266

63-
// handle site theme switching
64-
useEffect(() => {
65-
// TODO theme integration via context
66-
// @ts-ignore
67-
window.znaiTheme.addChangeHandler(onThemeChange);
68-
69-
// @ts-ignore
70-
return () => window.znaiTheme.removeChangeHandler(onThemeChange);
71-
72-
function onThemeChange() {
73-
injectCssProperties(iframeRef, dark, light);
74-
updateScrollBarToMatch(containerRef, iframeRef);
75-
}
76-
}, [dark, light]);
67+
const { syncTheme } = useIframeThemeSync(iframeRef, dark, light, () => {
68+
updateScrollBarToMatch(containerRef, iframeRef);
69+
});
7770

7871
useEffect(() => {
7972
return () => {
@@ -89,8 +82,15 @@ export function IframeFit({ src, title, wide, height, maxHeight, light, dark, pr
8982
activeElement = document.activeElement;
9083
}
9184

85+
const titleActions = (zoomEnabled || newTabEnabled) ? (
86+
<>
87+
{zoomEnabled && <Icon id="maximize-2" onClick={zoomIframe} />}
88+
{newTabEnabled && <Icon id="external-link" onClick={openInNewTab} />}
89+
</>
90+
) : undefined;
91+
9292
return (
93-
<Container wide={wide} title={title} additionalTitleClassNames="znai-iframe-title">
93+
<Container wide={wide} title={title} additionalTitleClassNames="znai-iframe-title" titleActions={titleActions}>
9494
<div ref={containerRef}></div>
9595
<iframe
9696
title={title}
@@ -104,6 +104,14 @@ export function IframeFit({ src, title, wide, height, maxHeight, light, dark, pr
104104
</Container>
105105
);
106106

107+
function zoomIframe() {
108+
zoom.zoom(<IframeZoomed src={src} title={title} light={light} dark={dark} />);
109+
}
110+
111+
function openInNewTab() {
112+
window.open(src, "_blank");
113+
}
114+
107115
function onLoad() {
108116
handleSize();
109117
updateScrollBarToMatch(containerRef, iframeRef);
@@ -153,7 +161,7 @@ export function IframeFit({ src, title, wide, height, maxHeight, light, dark, pr
153161
setTimeout(() => {
154162
const newHeight = measureContentHeight();
155163

156-
injectCssProperties(iframeRef, dark, light);
164+
syncTheme();
157165
setCalculatedIframeHeight(newHeight);
158166
setVisible(true);
159167

@@ -168,6 +176,26 @@ export function IframeFit({ src, title, wide, height, maxHeight, light, dark, pr
168176
}
169177
}
170178

179+
function useIframeThemeSync(iframeRef: React.RefObject<HTMLIFrameElement | null>, dark: any, light: any, onThemeChangeExtra?: () => void) {
180+
useEffect(() => {
181+
// @ts-ignore
182+
window.znaiTheme.addChangeHandler(onThemeChange);
183+
// @ts-ignore
184+
return () => window.znaiTheme.removeChangeHandler(onThemeChange);
185+
186+
function onThemeChange() {
187+
syncTheme();
188+
onThemeChangeExtra?.();
189+
}
190+
}, [dark, light]);
191+
192+
function syncTheme() {
193+
injectCssProperties(iframeRef, dark, light);
194+
}
195+
196+
return { syncTheme };
197+
}
198+
171199
function updateScrollBarToMatch(containerRef: any, iframeRef: any) {
172200
const div = containerRef!.current;
173201

@@ -260,6 +288,27 @@ export function calcAspectRatioPaddingTop(aspectRatio: string): string {
260288
return ((Number(height) / Number(width)) * 100.0).toFixed(2) + "%";
261289
}
262290

291+
function IframeZoomed({ src, title, light, dark }: Pick<Props, "src" | "title" | "light" | "dark">) {
292+
const iframeRef = useRef<HTMLIFrameElement>(null);
293+
const { syncTheme } = useIframeThemeSync(iframeRef, dark, light);
294+
295+
return (
296+
<div className="znai-iframe-zoomed" onClick={(e) => e.stopPropagation()}>
297+
<div className="znai-iframe-zoomed-header">
298+
<span className="znai-iframe-zoomed-title">{title}</span>
299+
<Icon id="x" className="znai-iframe-zoomed-close" onClick={() => zoom.clearZoom()} />
300+
</div>
301+
<iframe
302+
title={title}
303+
src={src}
304+
className="znai-iframe-zoomed-content"
305+
onLoad={syncTheme}
306+
ref={iframeRef}
307+
/>
308+
</div>
309+
);
310+
}
311+
263312
export const presentationIframe = {
264313
component: Iframe,
265314
numberOfSlides: () => 1,

0 commit comments

Comments
 (0)