Files
edx-platform/cms/static/js/views/xblock.js
Pooja Kulkarni f5b246d0e9 feat: copy/paste unit from within a unit in Studio - feature flagged (#33724)
(requires the contentstore.enable_copy_paste_units waffle flag)
2023-12-01 11:33:34 -08:00

260 lines
10 KiB
JavaScript

define(['jquery',
'underscore',
'common/js/components/utils/view_utils',
'js/views/baseview',
'xblock/runtime.v1',
'edx-ui-toolkit/js/utils/html-utils'],
function($, _, ViewUtils, BaseView, XBlock, HtmlUtils) {
'use strict';
var XBlockView = BaseView.extend({
// takes XBlockInfo as a model
events: {
'click .notification-action-button': 'fireNotificationActionEvent'
},
options: {
clipboardData: { content: null },
},
initialize: function() {
BaseView.prototype.initialize.call(this);
this.view = this.options.view;
},
render: function(options) {
var self = this,
view = this.view,
xblockInfo = this.model,
xblockUrl = xblockInfo.url(),
querystring = window.location.search; // pass any querystring down to child views
return $.ajax({
url: decodeURIComponent(xblockUrl) + '/' + view + querystring,
type: 'GET',
cache: false,
headers: {Accept: 'application/json'},
success: function(fragment) {
self.handleXBlockFragment(fragment, options);
}
});
},
initRuntimeData: function(xblock, options) {
if (options && options.initRuntimeData && xblock && xblock.runtime && !xblock.runtime.page) {
xblock.runtime.page = options.initRuntimeData;
}
return xblock;
},
handleXBlockFragment: function(fragment, options) {
var self = this,
wrapper = this.$el,
xblockElement,
successCallback = options ? options.success || options.done : null,
errorCallback = options ? options.error || options.done : null,
xblock,
fragmentsRendered,
aside;
fragmentsRendered = this.renderXBlockFragment(fragment, wrapper);
fragmentsRendered.always(function() {
xblockElement = self.$('.xblock').first();
try {
xblock = XBlock.initializeBlock(xblockElement);
self.xblock = self.initRuntimeData(xblock, options);
self.xblockReady(self.xblock);
self.$('.xblock_asides-v1').each(function() {
if (!$(this).hasClass('xblock-initialized')) {
aside = XBlock.initializeBlock($(this));
self.initRuntimeData(aside, options);
}
});
if (successCallback) {
successCallback(xblock);
}
} catch (e) {
console.error(e, e.stack);
// Add 'xblock-initialization-failed' class to every xblock
self.$('.xblock').addClass('xblock-initialization-failed');
// If the xblock was rendered but failed then still call xblockReady to allow
// drag-and-drop to be initialized.
if (xblockElement) {
self.xblockReady(null);
}
if (errorCallback) {
errorCallback();
}
}
});
},
/**
* Sends a notification event to the runtime, if one is available. Note that the runtime
* is only available once the xblock has been rendered and successfully initialized.
* @param eventName The name of the event to be fired.
* @param data The data to be passed to any listener's of the event.
*/
notifyRuntime: function(eventName, data) {
var runtime = this.xblock && this.xblock.runtime,
xblockChildren;
if (runtime) {
runtime.notify(eventName, data);
} else if (this.xblock) {
xblockChildren = this.xblock.element && $(this.xblock.element).prop('xblock_children');
if (xblockChildren) {
$(xblockChildren).each(function() {
if (this.runtime) {
this.runtime.notify(eventName, data);
}
});
}
}
},
/**
* This method is called upon successful rendering of an xblock. Note that the xblock
* may have thrown JavaScript errors after rendering in which case the xblock parameter
* will be null.
*/
xblockReady: function(xblock) { // eslint-disable-line no-unused-vars
// Do nothing
},
/**
* Renders an xblock fragment into the specified element. The fragment has two attributes:
* html: the HTML to be rendered
* resources: any JavaScript or CSS resources that the HTML depends upon
* Note that the XBlock is rendered asynchronously, and so a promise is returned that
* represents this process.
* @param fragment The fragment returned from the xblock_handler
* @param element The element into which to render the fragment (defaults to this.$el)
* @returns {Promise} A promise representing the rendering process
*/
renderXBlockFragment: function(fragment, element) {
var html = fragment.html,
resources = fragment.resources,
blockView = this;
if (!element) {
element = this.$el;
}
// Render the HTML first as the scripts might depend upon it, and then
// asynchronously add the resources to the page. Any errors that are thrown
// by included scripts are logged to the console but are then ignored assuming
// that at least the rendered HTML will be in place.
try {
return this.addXBlockFragmentResources(resources).done(function() {
console.log('Updating HTML');
try {
blockView.updateHtml(element, html);
} catch (e) {
console.error(e, e.stack);
}
});
} catch (e) {
console.error(e, e.stack);
return $.Deferred().resolve();
}
},
/**
* Updates an element to have the specified HTML. The default method sets the HTML
* as child content, but this can be overridden.
* @param element The element to be updated
* @param html The desired HTML.
*/
updateHtml: function(element, html) {
HtmlUtils.setHtml(element, HtmlUtils.HTML(html));
},
/**
* Dynamically loads all of an XBlock's dependent resources. This is an asynchronous
* process so a promise is returned.
* @param resources The resources to be rendered
* @returns {Promise} A promise representing the rendering process
*/
addXBlockFragmentResources: function(resources) {
var self = this,
applyResource,
numResources,
deferred;
numResources = resources.length;
deferred = $.Deferred();
applyResource = function(index) {
var hash, resource, value, promise;
if (index >= numResources) {
deferred.resolve();
return;
}
value = resources[index];
hash = value[0];
if (!window.loadedXBlockResources) {
window.loadedXBlockResources = [];
}
if (_.indexOf(window.loadedXBlockResources, hash) < 0) {
resource = value[1];
promise = self.loadResource(resource);
window.loadedXBlockResources.push(hash);
promise.done(function() {
applyResource(index + 1);
}).fail(function() {
deferred.reject();
});
} else {
applyResource(index + 1);
}
};
applyResource(0);
return deferred.promise();
},
/**
* Loads the specified resource into the page.
* @param resource The resource to be loaded.
* @returns {Promise} A promise representing the loading of the resource.
*/
loadResource: function(resource) {
var $head = $('head'),
mimetype = resource.mimetype,
kind = resource.kind,
placement = resource.placement,
data = resource.data;
if (mimetype === 'text/css') {
if (kind === 'text') {
// xss-lint: disable=javascript-jquery-append,javascript-concat-html
$head.append('<style type="text/css">' + data + '</style>');
} else if (kind === 'url') {
// xss-lint: disable=javascript-jquery-append,javascript-concat-html
$head.append('<link rel="stylesheet" href="' + data + '" type="text/css">');
}
} else if (mimetype === 'application/javascript') {
if (kind === 'text') {
// xss-lint: disable=javascript-jquery-append,javascript-concat-html
$head.append('<script>' + data + '</script>');
} else if (kind === 'url') {
return ViewUtils.loadJavaScript(data);
}
} else if (mimetype === 'text/html') {
if (placement === 'head') {
HtmlUtils.append($head, HtmlUtils.HTML(data));
}
}
// Return an already resolved promise for synchronous updates
return $.Deferred().resolve().promise();
},
fireNotificationActionEvent: function(event) {
var eventName = $(event.currentTarget).data('notification-action');
if (eventName) {
event.preventDefault();
this.notifyRuntime(eventName, this.model.get('id'));
}
}
});
return XBlockView;
}); // end define();