From e50b1e89e946c66dda78026247ca2fd4501da725 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 31 Jan 2024 18:01:12 +0100 Subject: [PATCH 1/4] feat: copy-paste XBlock rendering from `frontend-app-library-authoring` --- .../course/sequence/XBlock/LibraryBlock.jsx | 171 ++++++++ .../course/sequence/XBlock/index.js | 2 + src/courseware/course/sequence/XBlock/wrap.js | 390 ++++++++++++++++++ .../sequence/XBlock/xblock-bootstrap.html | 65 +++ webpack.dev.config.js | 26 ++ webpack.prod.config.js | 12 + 6 files changed, 666 insertions(+) create mode 100644 src/courseware/course/sequence/XBlock/LibraryBlock.jsx create mode 100644 src/courseware/course/sequence/XBlock/index.js create mode 100644 src/courseware/course/sequence/XBlock/wrap.js create mode 100644 src/courseware/course/sequence/XBlock/xblock-bootstrap.html create mode 100644 webpack.dev.config.js diff --git a/src/courseware/course/sequence/XBlock/LibraryBlock.jsx b/src/courseware/course/sequence/XBlock/LibraryBlock.jsx new file mode 100644 index 0000000000..d1d3552d99 --- /dev/null +++ b/src/courseware/course/sequence/XBlock/LibraryBlock.jsx @@ -0,0 +1,171 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ensureConfig, getConfig } from '@edx/frontend-platform'; + +import wrapBlockHtmlForIFrame from './wrap'; + +ensureConfig(['LMS_BASE_URL', 'SECURE_ORIGIN_XBLOCK_BOOTSTRAP_HTML_URL'], 'library block component'); + +/** + * 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. + */ +class LibraryBlock extends React.Component { + constructor(props) { + super(props); + this.iframeRef = React.createRef(); + this.state = { + html: null, + iFrameHeight: 400, + iframeKey: 0, + }; + } + + /** + * Load the XBlock data from the LMS and then inject it into our IFrame. + */ + componentDidMount() { + // Prepare to receive messages from the IFrame. + // Messages are the only way that the code in the IFrame can communicate + // with the surrounding UI. + window.addEventListener('message', this.receivedWindowMessage); + + // Process the XBlock view: + this.processView(); + } + + componentDidUpdate(prevProps) { + if (prevProps.view !== this.props.view) { + this.processView(); + } + } + + componentWillUnmount() { + window.removeEventListener('message', this.receivedWindowMessage); + } + + /** + * Handle any messages we receive from the XBlock Runtime code in the IFrame. + * See wrap.ts to see the code that sends these messages. + */ + receivedWindowMessage = async (event) => { + if (this.iframeRef.current === null || event.source !== this.iframeRef.current.contentWindow) { + return; // This is some other random message. + } + + const { method, replyKey, ...args } = event.data; + const frame = this.iframeRef.current.contentWindow; + const sendReply = async (data) => { + frame.postMessage({ ...data, replyKey }, '*'); + }; + + if (method === 'bootstrap') { + sendReply({ initialHtml: this.state.html }); + } else if (method === 'get_handler_url') { + const handlerUrl = await this.props.getHandlerUrl(args.usageId); + sendReply({ handlerUrl }); + } else if (method === 'update_frame_height') { + this.setState({ iFrameHeight: args.height }); + } else if (method?.indexOf('xblock:') === 0) { + // This is a notification from the XBlock's frontend via 'runtime.notify(event, args)' + if (this.props.onBlockNotification) { + this.props.onBlockNotification({ + eventType: method.substr(7), // Remove the 'xblock:' prefix that we added in wrap.ts + ...args, + }); + } + } + }; + + processView() { + const { view } = this.props; + if (view.value) { + const html = wrapBlockHtmlForIFrame( + view.value.content, + view.value.resources, + getConfig().LMS_BASE_URL, + ); + + // Load the XBlock HTML into the IFrame: + // iframe will only re-render in react when its property changes (key here) + this.setState(prevState => ({ + html, + iframeKey: prevState.iframeKey + 1, + })); + } + } + + render() { + /* 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 (this.state.html === null) { + return null; + } + + return ( +
+