Files
edx-platform/common/templates/xblock_v2/xblock_iframe.html

498 lines
23 KiB
HTML

<!DOCTYPE html>
<!-- Set the height of the iframe to 100% of the viewport height to avoid an extra scrollbar -->
<html style="height: 100vh;">
<head>
<!-- Open links in a new tab, not this iframe -->
<base target="_blank">
<meta charset="UTF-8">
<!-- gettext & XBlock JS i18n code -->
{% if is_development %}
<!-- in development, the djangojs file isn't available so use fallback-->
<script type="text/javascript" src="{{ lms_root_url }}/static/js/src/gettext_fallback.js"></script>
{% else %}
<script type="text/javascript" src="{{ lms_root_url }}/static/js/i18n/en/djangojs.js"></script>
{% endif %}
<!-- Most XBlocks require jQuery: -->
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<!-- ORA2 blocks require timepicker -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-timepicker/1.13.18/jquery.timepicker.min.js"></script>
<!-- The Video XBlock requires "ajaxWithPrefix" -->
<script type="text/javascript">
$.postWithPrefix = $.post;
$.getWithPrefix = $.get;
$.ajaxWithPrefix = $.ajax;
</script>
<!-- The Video XBlock requires "Slider" from jQuery-UI: -->
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<!-- The video XBlock depends on Underscore.JS -->
<script type="text/javascript" src="{{ lms_root_url }}/static/common/js/vendor/underscore.js"></script>
<!-- The video XBlock depends on jquery-cookie -->
<script type="text/javascript" src="{{ lms_root_url }}/static/js/vendor/jquery.cookie.js"></script>
<!--The Video XBlock has an undeclared dependency on 'Logger' -->
<script>
window.Logger = { log: function() { } };
</script>
<!-- Builtin XBlock types depend on RequireJS -->
<script type="text/javascript" src="{{ lms_root_url }}/static/common/js/vendor/require.js"></script>
<script type="text/javascript" src="{{ lms_root_url }}/static/js/RequireJS-namespace-undefine.js"></script>
<script>
// The minimal RequireJS configuration required for common LMS and CMS building XBlock types to work:
require = require || RequireJS.require;
define = define || RequireJS.define;
(function (require, define) {
if ('{{ view_name | safe }}' === 'studio_view') {
// Call `require-config.js` of the CMS
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = "{{ cms_root_url }}/static/studio/cms/js/require-config.js";
document.head.appendChild(script);
require.config({
baseUrl: "{{ cms_root_url }}/static/studio",
paths: {
accessibility: 'js/src/accessibility_tools',
draggabilly: 'js/vendor/draggabilly',
hls: 'common/js/vendor/hls',
moment: 'common/js/vendor/moment-with-locales',
'tinymce': 'js/vendor/tinymce/js/tinymce/tinymce.full.min',
'jquery.tinymce': 'js/vendor/tinymce/js/tinymce/jquery.tinymce.min',
},
});
require([
"{{ lms_root_url }}/static/dist{{ oa_manifest.oa_editor_tinymce_js }}",
"{{ lms_root_url }}/static/dist{{ oa_manifest.oa_editor_textarea_js }}",
]);
} else {
require.config({
baseUrl: "{{ lms_root_url }}/static/",
paths: {
accessibility: 'js/src/accessibility_tools',
draggabilly: 'js/vendor/draggabilly',
hls: 'common/js/vendor/hls',
moment: 'common/js/vendor/moment-with-locales',
HtmlUtils: 'edx-ui-toolkit/js/utils/html-utils',
},
});
}
define('gettext', [], function() { return window.gettext; });
define('jquery', [], function() { return window.jQuery; });
define('jquery-migrate', [], function() { return window.jQuery; });
define('underscore', [], function() { return window._; });
}).call(this, require, define);
</script>
<!-- edX HTML Utils requires GlobalLoader -->
<script type="text/javascript" src="{{ lms_root_url }}/static/edx-ui-toolkit/js/utils/global-loader.js"></script>
<script>
// The video XBlock has an undeclared dependency on edX HTML Utils
RequireJS.require(['HtmlUtils'], function (HtmlUtils) {
window.edx.HtmlUtils = HtmlUtils;
// The problem XBlock depends on window.SR, though 'accessibility_tools' has an undeclared dependency on HtmlUtils:
RequireJS.require(['accessibility']);
});
RequireJS.require(['edx-ui-toolkit/js/utils/string-utils'], function (StringUtils) {
window.edx.StringUtils = StringUtils;
});
</script>
<!--
commons.js: this file produced by webpack contains many shared chunks of code.
By including this, you have only to also import any of the smaller entrypoint
files (defined in webpack.common.config.js) to get that entry point and all
of its dependencies.
-->
<script type="text/javascript" src="{{ lms_root_url }}/static/bundles/commons.js"></script>
<!-- The video XBlock (and perhaps others?) expect this global: -->
<script>
window.onTouchBasedDevice = function() { return navigator.userAgent.match(/iPhone|iPod|iPad|Android/i); };
</script>
<!-- At least one XBlock (drag and drop v2) expects Font Awesome -->
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- Capa Problem Editing requires CodeMirror -->
<link rel="stylesheet" href="{{ lms_root_url }}/static/js/vendor/CodeMirror/codemirror.css">
<!-- Additional CSS for the XBlock Editor on the Library Authoring -->
<link rel="stylesheet" href="{{ cms_root_url }}/static/studio/css/vendor/normalize.css">
<link rel="stylesheet" href="{{ cms_root_url }}/static/studio/css/vendor/hint.css">
<link rel="stylesheet" href="{{ cms_root_url }}/static/studio/css/studio-main-v1.css" />
<link rel="stylesheet" href="{{ cms_root_url }}/static/studio/css/course-unit-mfe-iframe-bundle.css" />
<link rel="stylesheet" href="{{ lms_root_url }}/static/dist{{ oa_manifest.oa_ltr_css }}">
<script type="text/javascript" src="{{ lms_root_url }}/static/dist{{ oa_manifest.oa_ltr_js }}"></script>
<!-- Configure and load MathJax -->
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
CommonHTML: { linebreaks: { automatic: true } },
SVG: { linebreaks: { automatic: true } },
"HTML-CSS": { linebreaks: { automatic: true } },
tex2jax: {
inlineMath: [
["\\(","\\)"],
['[mathjaxinline]','[/mathjaxinline]']
],
displayMath: [
["\\[","\\]"],
['[mathjax]','[/mathjax]']
]
}
});
window.addEventListener('resize', MJrenderer);
let t = -1;
let delay = 1000;
let oldWidth = document.documentElement.scrollWidth;
function MJrenderer() {
// don't rerender if the window is the same size as before
if (t >= 0) {
window.clearTimeout(t);
}
if (oldWidth !== document.documentElement.scrollWidth) {
t = window.setTimeout(function() {
oldWidth = document.documentElement.scrollWidth;
MathJax.Hub.Queue(
["Rerender", MathJax.Hub],
);
t = -1;
}, delay);
}
};
</script>
<script type="text/x-mathjax-config">
MathJax.Hub.signal.Interest(function(message) {
if(message[0] === "End Math") {
set_mathjax_display_div_settings();
}
});
function set_mathjax_display_div_settings() {
$('.MathJax_Display').each(function( index ) {
this.setAttribute('tabindex', '0');
this.setAttribute('aria-live', 'off');
this.removeAttribute('role');
this.removeAttribute('aria-readonly');
});
}
</script>
<script type="text/javascript">
// Activating Mathjax accessibility files
window.MathJax = {
menuSettings: {
collapsible: true,
autocollapse: false,
explorer: true
}
};
</script>
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
MathJax extension libraries -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js?config=TeX-MML-AM_SVG"></script>
<!-- fragment head -->
{{ fragment.head_html | safe }}
</head>
<!-- The default stylesheet will set the body min-height to 100% (a common strategy to allow for background
images to fill the viewport), but this has the undesireable side-effect of causing an infinite loop via the onResize
event listeners below, in certain situations. Resetting it to the default "auto" skirts the problem.-->
<body style="background-color: white;" class="view-container">
<div id="content" class="wrapper xblock-iframe-content">
<!-- fragment body -->
{{ fragment.body_html | safe }}
<!-- fragment foot -->
{{ fragment.foot_html | safe }}
</div>
<script>
/**
* Map of all URL handlers for this block and its children, keyed by usage
* key.
*/
{% comment %}
This variable is expected to be a valid JSON, which will be translated
directly into a javascript object.
{% endcomment %}
HANDLER_URL_MAP = {{ handler_urls_json | safe }};
/**
* The JavaScript code which runs inside our IFrame and is responsible
* for communicating with the parent window.
*
* This cannot use any imported functions because it runs in the IFrame,
* not in our app webpack bundle.
*/
function blockFrameJS() {
const CHILDREN_KEY = '_jsrt_xb_children'; // JavaScript RunTime XBlock children
const USAGE_ID_KEY = '_jsrt_xb_usage_id';
const HANDLER_URL = '_jsrt_xb_handler_url';
const uniqueKeyPrefix = `k${+Date.now()}-${Math.floor(Math.random() * 1e10)}-`;
let messageCount = 0;
/**
* The JavaScript runtime for any XBlock in the IFrame
*/
const runtime = {
/**
* An obscure and little-used API that retrieves a particular
* XBlock child using its 'data-name' attribute
* @param block The root DIV element of the XBlock calling this method
* @param childName The value of the 'data-name' attribute of the root
* DIV element of the XBlock child in question.
*/
childMap: (block, childName) => runtime.children(block).find((child) => child.element.getAttribute('data-name') === childName),
children: (block) => block[CHILDREN_KEY],
/**
* Get the URL for the specified handler. This method must be synchronous, so
* cannot make HTTP requests.
*/
handlerUrl: (block, handlerName, suffix, query) => {
let url = block[HANDLER_URL].replace('handler_name', handlerName);
if (suffix) {
url += `${suffix}/`;
}
if (query) {
url += `?${query}`;
}
return url;
},
notify: (eventName, data) => {
/**
* Used in `studio_view` to notify events and errors
**/
window.parent.postMessage({
type: 'xblock-event',
eventName,
data,
}, '*');
}
};
/**
* Initialize an XBlock. This function should only be called by initializeXBlockAndChildren
* because it assumes that function has already run.
*/
function initializeXBlock(element, callback) {
const usageId = element[USAGE_ID_KEY];
// Check if the XBlock has an initialization function:
const initFunctionName = element.getAttribute('data-init');
if (initFunctionName !== null) {
// Since this block has an init function, it may need to call handlers:
element[HANDLER_URL] = HANDLER_URL_MAP[usageId];
// Now proceed with initializing the block's JavaScript:
const InitFunction = (window)[initFunctionName];
// Does the XBlock HTML contain arguments to pass to the InitFunction?
let data = {};
[].forEach.call(element.children, (childNode) => {
// The newer/pure/LearningCore runtime uses 'xblock_json_init_args'
// while the LMS runtime uses 'xblock-json-init-args'.
if (
childNode.matches('script.xblock_json_init_args')
|| childNode.matches('script.xblock-json-init-args')
) {
data = JSON.parse(childNode.textContent);
}
});
// An unfortunate inconsistency is that the old Studio runtime used
// to pass 'element' as a jQuery-wrapped DOM element, whereas the LMS
// runtime used to pass 'element' as the pure DOM node. In order not to
// break backwards compatibility, we would need to maintain that.
// However, this is currently disabled as it causes issues (need to
// modify the runtime methods like handlerUrl too), and we decided not
// to maintain support for legacy studio_view in this runtime.
// const isStudioView = element.className.indexOf('studio_view') !== -1;
// const passElement = isStudioView && (window as any).$ ? (window as any).$(element) : element;
const blockJS = new InitFunction(runtime, element, data) || {};
blockJS.element = element;
if (['MetadataOnlyEditingDescriptor', 'SequenceDescriptor'].includes(data['xmodule-type'])) {
// The xmodule type `MetadataOnlyEditingDescriptor` and `SequenceDescriptor` renders a `<div>` with
// the block metadata in the `data-metadata` attribute. But is necessary
// to call `XBlockEditorView.xblockReady()` to run the scripts to build the
// editor using the metadata.
require(['{{ cms_root_url }}/static/studio/js/views/xblock_editor.js'], function(XBlockEditorView) {
var editorView = new XBlockEditorView({
el: element,
xblock: blockJS,
});
// To render block using metadata
editorView.xblockReady(blockJS);
// Adding cancel and save buttons
var xblockActions = `
<div class="xblock-actions">
<ul>
<li class="action-item">
<input id="poll-submit-options" type="submit" class="button action-primary save-button" value="Save" onclick="return false;">
</li>
<li class="action-item">
<a href="#" class="button cancel-button">Cancel</a>
</li>
</ul>
</div>
`;
element.innerHTML += xblockActions;
const views = editorView.getMetadataEditor().views;
Object.values(views).forEach(view => {
const uniqueId = view.uniqueId;
const input = element.querySelector(`#${uniqueId}`);
if (input) {
input.addEventListener("input", function(event) {
view.model.setValue(event.target.value);
});
}
});
// Adding cancel functionality
$('.cancel-button', element).bind('click', function() {
runtime.notify('cancel', {});
event.preventDefault();
});
// Adding save functionality
$('.save-button', element).bind('click', function() {
//event.preventDefault();
var error_message_div = $('.xblock-editor-error-message', element);
const modifiedData = editorView.getChangedMetadata();
error_message_div.html();
error_message_div.css('display', 'none');
var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
runtime.notify('save', {state: 'start', message: gettext("Saving")});
$.post(handlerUrl, JSON.stringify(modifiedData)).done(function(response) {
if (response.result === 'success') {
runtime.notify('save', {state: 'end'})
window.location.reload(false);
} else {
runtime.notify('error', {
'title': 'Error saving changes',
'message': response.message,
});
error_message_div.html('Error: '+response.message);
error_message_div.css('display', 'block');
}
});
});
});
}
callback(blockJS);
} else {
const blockJS = { element };
callback(blockJS);
}
if ('{{ view_name | safe }}' === 'studio_view') {
// Used when rendering the `studio_view`, in order to avoid open a new tab on click cancel or save
const selectors = [
'.cancel-button',
'.save-button',
'.action-cancel',
'.action-save',
'.openassessment_cancel_button',
'.openassessment_save_button',
];
for (const selector of selectors) {
const queryObject = document.querySelector(selector);
if (queryObject) {
queryObject.addEventListener('click', function() {
event.preventDefault();
});
}
}
}
}
// Recursively initialize the JavaScript code of each XBlock:
function initializeXBlockAndChildren(element, callback) {
// The newer/pure/LearningCore runtime uses the 'data-usage' attribute, while the LMS uses 'data-usage-id'
const usageId = element.getAttribute('data-usage') || element.getAttribute('data-usage-id');
if (usageId !== null) {
element[USAGE_ID_KEY] = usageId;
} else {
throw new Error('XBlock is missing a usage ID attribute on its root HTML node.');
}
const version = element.getAttribute('data-runtime-version');
if (version != null && version !== '1') {
throw new Error('Unsupported XBlock runtime version requirement.');
}
// Recursively initialize any children first:
// We need to find all div.xblock-v1 children, unless they're grandchilden
// So we build a list of all div.xblock-v1 descendants that aren't descendants
// of an already-found descendant:
const childNodesFound = [];
[].forEach.call(element.querySelectorAll('.xblock, .xblock-v1'), (childNode) => {
if (!childNodesFound.find((el) => el.contains(childNode))) {
childNodesFound.push(childNode);
}
});
// This code is awkward because we can't use promises (IE11 etc.)
let childrenInitialized = -1;
function initNextChild() {
childrenInitialized += 1;
if (childrenInitialized < childNodesFound.length) {
const childNode = childNodesFound[childrenInitialized];
initializeXBlockAndChildren(childNode, initNextChild);
} else {
// All children are initialized:
initializeXBlock(element, callback);
}
}
initNextChild();
}
// Find the root XBlock node.
// The newer/pure/LearningCore runtime uses '.xblock-v1' while the LMS runtime uses '.xblock'.
const rootNode = document.querySelector('.xblock, .xblock-v1'); // will always return the first matching element
initializeXBlockAndChildren(rootNode, () => {
});
(function() {
// If this view is rendered in an iframe within the authoring microfrontend app
// it will report the height of its contents to the parent window when the
// document loads, window resizes, or DOM mutates.
if (window !== window.parent) {
function dispatchResizeMessage(event) {
// Note: event is actually an Array of MutationRecord objects when fired from the MutationObserver
var newHeight = rootNode.scrollHeight;
var newWidth = rootNode.offsetWidth;
window.parent.postMessage(
{
type: 'plugin.resize',
payload: {
width: newWidth,
height: newHeight,
}
}, document.referrer
);
// Within the authoring microfrontend the iframe resizes to match the
// height of this document and it should never scroll. It does scroll
// ocassionally when javascript is used to focus elements on the page
// before the parent iframe has been resized to match the content
// height. This window.scrollTo is an attempt to keep the content at the
// top of the page.
window.scrollTo(0, 0);
}
// Create an observer instance linked to the callback function
const observer = new MutationObserver(dispatchResizeMessage);
// Start observing the target node for configured mutations
observer.observe(rootNode, { attributes: true, childList: true, subtree: true });
const resizeObserver = new ResizeObserver(dispatchResizeMessage);
resizeObserver.observe(rootNode);
}
}());
}
window.addEventListener('load', blockFrameJS);
</script>
</body>
</html>