xblock.js 10.3 KB
Newer Older
1 2
define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/views/baseview', 'xblock/runtime.v1'],
    function($, _, ViewUtils, BaseView, XBlock) {
3
        'use strict';
4 5 6 7

        var XBlockView = BaseView.extend({
            // takes XBlockInfo as a model

8
            events: {
9
                'click .notification-action-button': 'fireNotificationActionEvent'
10 11
            },

12 13 14 15 16
            initialize: function() {
                BaseView.prototype.initialize.call(this);
                this.view = this.options.view;
            },

17
            render: function(options) {
18
                var self = this,
19 20 21
                    view = this.view,
                    xblockInfo = this.model,
                    xblockUrl = xblockInfo.url();
22
                return $.ajax({
23
                    url: decodeURIComponent(xblockUrl) + '/' + view,
24
                    type: 'GET',
25
                    cache: false,
26
                    headers: {Accept: 'application/json'},
27
                    success: function(fragment) {
28
                        self.handleXBlockFragment(fragment, options);
29 30 31 32
                    }
                });
            },

33 34 35 36 37 38 39
            initRuntimeData: function(xblock, options) {
                if (options && options.initRuntimeData && xblock && xblock.runtime && !xblock.runtime.page) {
                    xblock.runtime.page = options.initRuntimeData;
                }
                return xblock;
            },

40
            handleXBlockFragment: function(fragment, options) {
41 42
                var self = this,
                    wrapper = this.$el,
43
                    xblockElement,
44 45
                    successCallback = options ? options.success || options.done : null,
                    errorCallback = options ? options.error || options.done : null,
46 47 48 49
                    xblock,
                    fragmentsRendered;

                fragmentsRendered = this.renderXBlockFragment(fragment, wrapper);
50
                fragmentsRendered.always(function() {
51
                    xblockElement = self.$('.xblock').first();
52
                    try {
53
                        xblock = XBlock.initializeBlock(xblockElement);
54 55 56
                        self.xblock = self.initRuntimeData(xblock, options);
                        self.xblockReady(self.xblock);
                        self.$('.xblock_asides-v1').each(function() {
57 58 59 60
                            if (!$(this).hasClass('xblock-initialized')) {
                                var aside = XBlock.initializeBlock($(this));
                                self.initRuntimeData(aside, options);
                            }
61
                        });
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
                        if (successCallback) {
                            successCallback(xblock);
                        }
                    } catch (e) {
                        console.error(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();
                        }
78 79
                    }
                });
80 81 82
            },

            /**
83 84 85 86
             * 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.
87
             */
88 89 90 91
            notifyRuntime: function(eventName, data) {
                var runtime = this.xblock && this.xblock.runtime;
                if (runtime) {
                    runtime.notify(eventName, data);
92 93 94
                } else if (this.xblock) {
                    var xblock_children = this.xblock.element && $(this.xblock.element).prop('xblock_children');
                    if (xblock_children) {
95
                        $(xblock_children).each(function() {
96 97 98 99 100
                            if (this.runtime) {
                                this.runtime.notify(eventName, data);
                            }
                        });
                    }
101
                }
102 103 104
            },

            /**
105 106 107
             * 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.
108
             */
109
            xblockReady: function(xblock) {  // eslint-disable-line no-unused-vars
110
                // Do nothing
111
            },
112 113

            /**
114
             * Renders an xblock fragment into the specified element. The fragment has two attributes:
115 116
             *   html: the HTML to be rendered
             *   resources: any JavaScript or CSS resources that the HTML depends upon
117 118
             * Note that the XBlock is rendered asynchronously, and so a promise is returned that
             * represents this process.
119 120
             * @param fragment The fragment returned from the xblock_handler
             * @param element The element into which to render the fragment (defaults to this.$el)
121
             * @returns {Promise} A promise representing the rendering process
122 123
             */
            renderXBlockFragment: function(fragment, element) {
124 125
                var html = fragment.html,
                    resources = fragment.resources;
126 127 128
                if (!element) {
                    element = this.$el;
                }
129 130

                // Render the HTML first as the scripts might depend upon it, and then
131 132 133 134 135 136
                // 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 {
                    this.updateHtml(element, html);
                    return this.addXBlockFragmentResources(resources);
137
                } catch (e) {
138 139 140
                    console.error(e.stack);
                    return $.Deferred().resolve();
                }
141
            },
142

143
            /**
144 145 146 147 148 149 150 151 152 153
             * 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) {
                element.html(html);
            },

            /**
154 155 156
             * 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
157
             * @returns {Promise} A promise representing the rendering process
158 159 160 161 162 163 164 165 166
             */
            addXBlockFragmentResources: function(resources) {
                var self = this,
                    applyResource,
                    numResources,
                    deferred;
                numResources = resources.length;
                deferred = $.Deferred();
                applyResource = function(index) {
167
                    var hash, resource, value, promise;
168 169 170 171 172
                    if (index >= numResources) {
                        deferred.resolve();
                        return;
                    }
                    value = resources[index];
173 174 175 176 177 178
                    hash = value[0];
                    if (!window.loadedXBlockResources) {
                        window.loadedXBlockResources = [];
                    }
                    if (_.indexOf(window.loadedXBlockResources, hash) < 0) {
                        resource = value[1];
179
                        promise = self.loadResource(resource);
180
                        window.loadedXBlockResources.push(hash);
181 182 183 184 185 186 187
                        promise.done(function() {
                            applyResource(index + 1);
                        }).fail(function() {
                            deferred.reject();
                        });
                    } else {
                        applyResource(index + 1);
188 189
                    }
                };
190 191 192
                applyResource(0);
                return deferred.promise();
            },
193

194 195 196
            /**
             * Loads the specified resource into the page.
             * @param resource The resource to be loaded.
197
             * @returns {Promise} A promise representing the loading of the resource.
198 199 200 201 202 203 204
             */
            loadResource: function(resource) {
                var head = $('head'),
                    mimetype = resource.mimetype,
                    kind = resource.kind,
                    placement = resource.placement,
                    data = resource.data;
205 206 207 208
                if (mimetype === 'text/css') {
                    if (kind === 'text') {
                        head.append("<style type='text/css'>" + data + '</style>');
                    } else if (kind === 'url') {
209 210
                        head.append("<link rel='stylesheet' href='" + data + "' type='text/css'>");
                    }
211 212 213 214
                } else if (mimetype === 'application/javascript') {
                    if (kind === 'text') {
                        head.append('<script>' + data + '</script>');
                    } else if (kind === 'url') {
215
                        return ViewUtils.loadJavaScript(data);
216
                    }
217 218
                } else if (mimetype === 'text/html') {
                    if (placement === 'head') {
219 220
                        head.append(data);
                    }
221
                }
222 223
                // Return an already resolved promise for synchronous updates
                return $.Deferred().resolve().promise();
224 225 226
            },

            fireNotificationActionEvent: function(event) {
227
                var eventName = $(event.currentTarget).data('notification-action');
228 229
                if (eventName) {
                    event.preventDefault();
230
                    this.notifyRuntime(eventName, this.model.get('id'));
231
                }
232 233 234 235 236
            }
        });

        return XBlockView;
    }); // end define();