260 lines
10 KiB
JavaScript
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();
|