Skip to content

Commit

Permalink
Merge pull request #2203 from tf/iframe-resize
Browse files Browse the repository at this point in the history
Add option to resize iframe embed based on message sent from embed
  • Loading branch information
tf authored Feb 12, 2025
2 parents 3d24a71 + 640a85e commit 73f1497
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 6 deletions.
12 changes: 10 additions & 2 deletions app/assets/stylesheets/pageflow/editor/info_box.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@
margin-bottom: 1em;
}

&.info,
&.error {
color: var(--ui-on-error-surface-color);
background-color: var(--ui-error-surface-color);
border-radius: rounded();
padding: space(3);
}

&.info {
background-color: var(--ui-selection-color-lighter);
}

&.error {
color: var(--ui-on-error-surface-color);
background-color: var(--ui-error-surface-color);
}

.shortcuts {
dt {
display: block;
Expand Down
15 changes: 15 additions & 0 deletions entry_types/scrolled/config/locales/new/iframe_resize.de.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
de:
pageflow_scrolled:
editor:
content_elements:
iframeEmbed:
attributes:
autoResize:
label: Dynamische Höhenanpassungen
inline_help: Höhe automatisch an den Inhalt des Elements anpassen.
help_texts:
autoResize: |-
Diese Option erfordert die Integration eines
Skripts in die eingebettete Seite. Weitere
Informationen findest du in diesem <a target="_blank" rel="noopener noreferrer"
href="https://pageflow.freshdesk.com/de/support/solutions/articles/11000130710-wie-passe-ich-die-gr%C3%B6%C3%9Fe-von-iframe-embeds-automatisch-an-den-inhalt-an-">Hilfeartikel</a>.
13 changes: 13 additions & 0 deletions entry_types/scrolled/config/locales/new/iframe_resize.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
en:
pageflow_scrolled:
editor:
content_elements:
iframeEmbed:
attributes:
autoResize:
label: Dynamic Height
inline_help: Adapt height to content automatically.
help_texts:
autoResize: |-
This option requires a custom script in the embedded
page. <a target="_blank" rel="noopener noreferrer" href="https://pageflow.freshdesk.com/en/support/solutions/articles/11000130710-how-to-automatically-resize-iframe-embeds-to-fit-the-content">More info</a>.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {useIframeHeight} from 'contentElements/iframeEmbed/useIframeHeight';

import {renderHook} from '@testing-library/react-hooks';
import {asyncHandlingOf} from 'support/asyncHandlingOf/forHooks';

describe('useIframeHeight', () => {
it('is undefined by default', async () => {
const {result} = renderHook(() =>
useIframeHeight({src: 'https://example.com/some/',
active: true})
);

expect(result.current).toEqual('400px');
});

it('sets height from message matching iframe location', async () => {
const {result} = renderHook(() =>
useIframeHeight({src: 'https://example.com/some/',
active: true})
);

await asyncHandlingOf(() => {
window.postMessage(JSON.stringify({
context: 'iframe.resize',
height: 350,
src: 'https://example.com/some/'
}), '*');
});

expect(result.current).toEqual('350px');
});

it('removes listener on cleanup', async () => {
const {result, unmount} = renderHook(() =>
useIframeHeight({src: 'https://example.com/some/',
active: true})
);

unmount();
await asyncHandlingOf(() => {
window.postMessage(JSON.stringify({
context: 'iframe.resize',
height: 350,
src: 'https://example.com/some/'
}), '*');
});

expect(result.current).toEqual('400px');
});

it('ignores messages if src does not match', async () => {
const {result} = renderHook(() =>
useIframeHeight({src: 'https://example.com/some/',
active: true})
);

await asyncHandlingOf(() => {
window.postMessage(JSON.stringify({
context: 'iframe.resize',
height: 350,
src: 'https://example.com/other/'
}), '*');
});

expect(result.current).toEqual('400px');
});

it('ignores messages if context is not iframe.resize', async () => {
const {result} = renderHook(() =>
useIframeHeight({src: 'https://example.com/some/',
active: true})
);

await asyncHandlingOf(() => {
window.postMessage(JSON.stringify({
context: 'whatever',
height: 350,
src: 'https://example.com/some/'
}), '*');
});

expect(result.current).toEqual('400px');
});

it('ignores non-string messages', async () => {
const {result} = renderHook(() =>
useIframeHeight({src: 'https://example.com/some/',
active: true})
);

await asyncHandlingOf(() => {
window.postMessage({
some: 'value'
}, '*');
});

expect(result.current).toEqual('400px');
});

it('ignores string messages that do not parse as JSON', async () => {
const {result} = renderHook(() =>
useIframeHeight({src: 'https://example.com/some/',
active: true})
);

await asyncHandlingOf(() => {
window.postMessage('hello', '*');
});

expect(result.current).toEqual('400px');
});

it('ignores messages if not active', async () => {
const {result} = renderHook(() =>
useIframeHeight({src: 'https://example.com/some/',
active: false})
);

await asyncHandlingOf(() => {
window.postMessage(JSON.stringify({
context: 'iframe.resize',
height: 350,
src: 'https://example.com/some/'
}), '*');
});

expect(result.current).toEqual('400px');
});

it('never regards undefined source as matching', async () => {
const {result} = renderHook(() =>
useIframeHeight({src: undefined,
active: true})
);

await asyncHandlingOf(() => {
window.postMessage(JSON.stringify({
context: 'iframe.resize',
height: 350
}), '*');
});

expect(result.current).toEqual('400px');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
utils
} from 'pageflow-scrolled/frontend';

import {useIframeHeight} from './useIframeHeight';

import styles from './IframeEmbed.module.css';

const aspectRatios = {
Expand All @@ -26,27 +28,46 @@ export function IframeEmbed({configuration}) {
const {shouldLoad} = useContentElementLifecycle();
const {isEditable, isSelected} = useContentElementEditorState();
const portraitOrientation = usePortraitOrientation();
const height = useIframeHeight({src: configuration.source,
active: configuration.autoResize})

const aspectRatio = portraitOrientation && configuration.portraitAspectRatio ?
configuration.portraitAspectRatio :
configuration.aspectRatio;

function renderSpanningWrapper(children) {
if (configuration.autoResize) {
return (
<div style={{height}}>
{children}
</div>
);
}
else {
return (
<FitViewport.Content>
{children}
</FitViewport.Content>
)
}
}

return (
<div className={styles.wrapper}
style={{pointerEvents: isEditable && !isSelected ? 'none' : undefined}}>
<FitViewport aspectRatio={aspectRatios[aspectRatio || 'wide']}
opaque={utils.isBlank(configuration.source)}>
<ContentElementBox>
<ContentElementFigure configuration={configuration}>
<FitViewport.Content>
{renderSpanningWrapper(
<ThirdPartyOptIn>
{shouldLoad &&
<iframe className={classNames(styles.iframe,
styles[`scale-${configuration.scale}`])}
title={configuration.title}
src={configuration.source} />}
</ThirdPartyOptIn>
</FitViewport.Content>
)}
<OptOutInfo configuration={configuration} />
</ContentElementFigure>
</ContentElementBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,24 @@ editor.contentElementTypes.register('iframeEmbed', {
});
this.input('title', TextInputView);
this.input('aspectRatio', SelectInputView, {
values: aspectRatios
values: aspectRatios,
disabledBinding: 'autoResize'
});
this.input('portraitAspectRatio', SelectInputView, {
includeBlank: true,
blankTranslationKey: 'pageflow_scrolled.editor.' +
'content_elements.iframeEmbed.' +
'attributes.portraitAspectRatio.blank',
values: aspectRatios
values: aspectRatios,
disabledBinding: 'autoResize'
});
this.input('autoResize', CheckBoxInputView);
this.view(InfoBoxView, {
level: 'info',
text: I18n.t(
'pageflow_scrolled.editor.content_elements.iframeEmbed.help_texts.autoResize'
),
visibleBinding: 'autoResize'
});
this.input('scale', SelectInputView, {
values: ['p100', 'p75', 'p50', 'p33']
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {useEffect, useState} from 'react';

export function useIframeHeight({src, active}) {
const [height, setHeight] = useState('400px');

useEffect(() => {
if (!active) {
return;
}

window.addEventListener('message', receive);

function receive(event) {
const data = parse(event.data);

if (src && data.context === 'iframe.resize' && data.src === src) {
setHeight(data.height + 'px');
}
}

return () => window.removeEventListener('message', receive);
}, [active, src]);

return height;
}

function parse(data) {
try {
return JSON.parse(data);
}
catch(e) {
return {};
}
}

0 comments on commit 73f1497

Please sign in to comment.