Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add component preview to library authoring [FC-0062] #1242

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/assets/scss/_utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@
.mw-300px {
max-width: 300px;
}

.right-0 {
right: 0;
}
141 changes: 141 additions & 0 deletions src/library-authoring/LibraryBlock/LibraryBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/* eslint-disable react/require-default-props */
import React, {
useEffect, useMemo, useRef, useState,
} from 'react';
import { ensureConfig, getConfig } from '@edx/frontend-platform';

import type { XBlockRenderResponse } from '../data/api';
import wrapBlockHtmlForIFrame from './wrap';

ensureConfig(['LMS_BASE_URL', 'SECURE_ORIGIN_XBLOCK_BOOTSTRAP_HTML_URL'], 'library block component');

interface LibraryBlockProps {
getHandlerUrl: (usageId: string) => Promise<string>;
onBlockNotification?: (event: { eventType: string; [key: string]: any }) => void;
view: XBlockRenderResponse
}
/**
* React component that displays an XBlock in a sandboxed IFrame.
*
* The IFrame is resized responsively so that it fits the content height.
*
* We use an IFrame so that the XBlock code, including user-authored HTML,
* cannot access things like the user's cookies, nor can it make GET/POST
* requests as the user. However, it is allowed to call any XBlock handlers.
*/
const LibraryBlock = ({ getHandlerUrl, onBlockNotification, view }: LibraryBlockProps) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [iFrameHeight, setIFrameHeight] = useState(400);

Check warning on line 28 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L27-L28

Added lines #L27 - L28 were not covered by tests

const html = useMemo(() => {

Check warning on line 30 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L30

Added line #L30 was not covered by tests
if (!view) {
return null;

Check warning on line 32 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L32

Added line #L32 was not covered by tests
}

return wrapBlockHtmlForIFrame(

Check warning on line 35 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L35

Added line #L35 was not covered by tests
view.content,
view.resources,
getConfig().LMS_BASE_URL,
);
}, [view]);

/**
* Handle any messages we receive from the XBlock Runtime code in the IFrame.
* See wrap.ts to see the code that sends these messages.
*/
/* istanbul ignore next */
const receivedWindowMessage = async (event) => {
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow) {
return; // This is some other random message.
}

const { method, replyKey, ...args } = event.data;
const frame = iframeRef.current.contentWindow;
const sendReply = async (data) => {
frame?.postMessage({ ...data, replyKey }, '*');
};

if (method === 'bootstrap') {
sendReply({ initialHtml: html });
} else if (method === 'get_handler_url') {
const handlerUrl = await getHandlerUrl(args.usageId);
sendReply({ handlerUrl });
} else if (method === 'update_frame_height') {
setIFrameHeight(args.height);
} else if (method?.indexOf('xblock:') === 0) {
// This is a notification from the XBlock's frontend via 'runtime.notify(event, args)'
if (onBlockNotification) {
onBlockNotification({
eventType: method.substr(7), // Remove the 'xblock:' prefix that we added in wrap.ts
...args,
});
}
}
};

/**
* Load the XBlock data from the LMS and then inject it into our IFrame.
rpenido marked this conversation as resolved.
Show resolved Hide resolved
*/
useEffect(() => {

Check warning on line 79 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L79

Added line #L79 was not covered by tests
// Prepare to receive messages from the IFrame.
rpenido marked this conversation as resolved.
Show resolved Hide resolved
// Messages are the only way that the code in the IFrame can communicate
// with the surrounding UI.
window.addEventListener('message', receivedWindowMessage);

Check warning on line 83 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L83

Added line #L83 was not covered by tests

return () => {
window.removeEventListener('message', receivedWindowMessage);

Check warning on line 86 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L85-L86

Added lines #L85 - L86 were not covered by tests
};
}, [view]);

/* Only draw the iframe if the HTML has already been set. This is because xblock-bootstrap.html will only request
* HTML once, upon being rendered. */
if (!html) {
return null;

Check warning on line 93 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L93

Added line #L93 was not covered by tests
}

return (

Check warning on line 96 in src/library-authoring/LibraryBlock/LibraryBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/LibraryBlock/LibraryBlock.tsx#L96

Added line #L96 was not covered by tests
<div style={{
height: `${iFrameHeight}px`,
boxSizing: 'content-box',
position: 'relative',
overflow: 'hidden',
minHeight: '200px',
border: '1px solid #ccc',
}}
>
<iframe
key={view.content}
ref={iframeRef}
title="block"
src={getConfig().SECURE_ORIGIN_XBLOCK_BOOTSTRAP_HTML_URL}
data-testid="block-preview"
style={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
minHeight: '200px',
border: '0 none',
backgroundColor: 'white',
}}
// allowing 'autoplay' is required to allow the video XBlock to control the YouTube iframe it has.
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
sandbox={[
'allow-forms',
'allow-modals',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin', // This is only secure IF the IFrame source
// is served from a completely different domain name
// e.g. labxchange-xblocks.net vs www.labxchange.org
'allow-scripts',
'allow-top-navigation-by-user-activation',
].join(' ')}
/>
</div>
);
};

export default LibraryBlock;
2 changes: 2 additions & 0 deletions src/library-authoring/LibraryBlock/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-disable-next-line import/prefer-default-export */
export { default as LibraryBlock } from './LibraryBlock';
Loading