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' }, 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(); return $.ajax({ url: decodeURIComponent(xblockUrl) + '/' + view, 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(''); } else if (kind === 'url') { // xss-lint: disable=javascript-jquery-append,javascript-concat-html $head.append(''); } } else if (mimetype === 'application/javascript') { if (kind === 'text') { // xss-lint: disable=javascript-jquery-append,javascript-concat-html $head.append(''); } 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();