course_info_update.js 15.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
define(['codemirror',
        'js/utils/modal',
        'js/utils/date_utils',
        'edx-ui-toolkit/js/utils/html-utils',
        'js/views/course_info_helper',
        'js/views/validation',
        'js/models/course_update',
        'common/js/components/views/feedback_prompt',
        'common/js/components/views/feedback_notification'],
    function(CodeMirror, ModalUtils, DateUtils, HtmlUtils, CourseInfoHelper, ValidatingView, CourseUpdateModel,
             PromptView, NotificationView) {
12 13
        'use strict';
        var CourseInfoUpdateView = ValidatingView.extend({
14

15
        // collection is CourseUpdateCollection
16 17 18 19 20 21 22 23 24 25
            events: {
                'click .new-update-button': 'onNew',
                'click .save-button': 'onSave',
                'click .cancel-button': 'onCancel',
                'click .post-actions > .edit-button': 'onEdit',
                'click .post-actions > .delete-button': 'onDelete'
            },

            initialize: function() {
                this.template = this.loadTemplate('course_info_update');
26 27

                // when the client refetches the updates as a whole, re-render them
28 29 30
                this.listenTo(this.collection, 'reset', this.render);
                this.listenTo(this.collection, 'invalid', this.handleValidationError);
            },
31

32
            render: function() {
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
                // iterate over updates and create views for each using the template
                var updateList = this.$el.find('#course-update-list'),
                    self = this;
                $(updateList).empty();
                if (this.collection.length > 0) {
                    this.collection.each(function(update, index) {
                        try {
                            CourseInfoHelper.changeContentToPreview(
                                update, 'content', self.options.base_asset_url);
                            // push notification is always disabled for existing updates
                            HtmlUtils.append(
                                updateList,
                                HtmlUtils.HTML(self.template({updateModel: update, push_notification_enabled: false}))
                            );
                            DateUtils.setupDatePicker('date', self, index);
                            update.isValid();
                        } catch (e) {
                            // ignore
                        } finally {
                            if (index === self.collection.length - 1) {
                                // Once the collection is loaded enable the button.
                                self.$el.find('.new-update-button').removeAttr('disabled');
                            }
                        }
                    });
                } else {
                    // If the collection is empty enable the New update button
                    self.$el.find('.new-update-button').removeAttr('disabled');
                }

                // Hide Update forms that are not for new updates with the editing class
                updateList.children().each(function(index, updateElement) {
                    var $updateElement = $(updateElement);
                    var updateForm = $updateElement.find('.new-update-form');
                    if ($updateElement.length > 0 && !$updateElement.hasClass('editing')) {
                        $(updateForm).hide();
69 70 71 72
                    }
                });
                return this;
            },
73

74 75 76
            collectionSelector: function(uid) {
                return 'course-update-list li[name=' + uid + ']';
            },
77

78 79
            setAndValidate: function(attr, value, event) {
                if (attr === 'date') {
80
                // If the value to be set was typed, validate that entry rather than the current datepicker value
81 82 83 84 85
                    if (this.dateEntry(event).length > 0) {
                        value = DateUtils.parseDateFromString(this.dateEntry(event).val());
                        if (value && isNaN(value.getTime())) {
                            value = '';
                        }
86
                    }
87
                    value = $.datepicker.formatDate('MM d, yy', value);
88
                }
89 90 91 92 93
                var targetModel = this.collection.get(this.$currentPost.attr('name'));
                var prevValue = targetModel.get(attr);
                if (prevValue !== value) {
                    targetModel.set(attr, value);
                    this.validateModel(targetModel);
94
                }
95
            },
96

97
            handleValidationError: function(model, error) {
98 99 100 101 102
                var self = this,
                    $validationElement = this.$el.find('#course-update-list li[name="' + model.cid + '"]');

                $validationElement.find('.message-error').remove();
                Object.keys(error).forEach(function(field) {
103
                    if (error.hasOwnProperty(field)) {
104 105 106 107 108 109 110 111
                        HtmlUtils.append(
                            $validationElement.find('#update-date-' + model.cid).parent(),
                            self.errorTemplate({message: error[field]})
                        );
                        HtmlUtils.append(
                            $validationElement.find('.date-display').parent(),
                            self.errorTemplate({message: error[field]})
                        );
112
                    }
113 114 115
                });

                $validationElement.find('.save-button').addClass('is-disabled');
116 117 118
            },

            validateModel: function(model) {
119
                var $validationElement = this.$el.find('#course-update-list li[name="' + model.cid + '"]');
120
                if (model.isValid()) {
121 122
                    $validationElement.find('.message-error').remove();
                    $validationElement.find('.save-button').removeClass('is-disabled');
123 124
                }
            },
125

126
            onNew: function(event) {
127
                // create new obj, insert into collection, and render this one ele overriding the hidden attr
128
                var newModel = new CourseUpdateModel();
129 130
                event.preventDefault();

131
                this.collection.add(newModel, {at: 0});
132

133
                var $newForm = $(
134
                this.template({
135 136
                    updateModel: newModel,
                    push_notification_enabled: this.options.push_notification_enabled
137
                })
138
                );
139

140 141
                var updateEle = this.$el.find('#course-update-list');
                $(updateEle).prepend($newForm);
142

143 144 145 146 147 148
                var $textArea = $newForm.find('.new-update-content').first();
                this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
                    mode: 'text/html',
                    lineNumbers: true,
                    lineWrapping: true
                });
149

150 151
                $newForm.addClass('editing');
                this.$currentPost = $newForm.closest('li');
152

153
                // Variable stored for unit test.
154
                this.$modalCover = ModalUtils.showModalCover(false, function() {
155
                // Binding empty function to prevent default hideModal.
156
                });
157

158 159
                DateUtils.setupDatePicker('date', this, 0);
            },
160

161 162 163 164
            onSave: function(event) {
                event.preventDefault();
                var targetModel = this.eventModel(event);
                targetModel.set({
165
                // translate short-form date (for input) into long form date (for display)
166 167 168 169
                    date: $.datepicker.formatDate('MM d, yy', new Date(this.dateEntry(event).val())),
                    content: this.$codeMirror.getValue(),
                    push_notification_selected: this.push_notification_selected(event)
                });
170
            // push change to display, hide the editor, submit the change
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
                var saving = new NotificationView.Mini({
                    title: gettext('Saving')
                });
                saving.show();
                var ele = this.modelDom(event);
                targetModel.save({}, {
                    success: function() {
                        saving.hide();
                    },
                    error: function() {
                        ele.remove();
                    }
                });
                this.closeEditor(false);

                analytics.track('Saved Course Update', {
                    'course': course_location_analytics,
                    'date': this.dateEntry(event).val(),
                    'push_notification_selected': this.push_notification_selected(event)
                });
            },

            onCancel: function(event) {
                event.preventDefault();
195
            // Since we're cancelling, the model should be using it's previous attributes
196 197 198
                var targetModel = this.eventModel(event);
                targetModel.set(targetModel.previousAttributes());
                this.validateModel(targetModel);
199
            // Hide the editor
200
                $(this.editor(event)).hide();
201
            // targetModel will be lacking an id if it was newly created
202 203 204 205 206 207 208 209 210 211 212 213
                this.closeEditor(!targetModel.id);
            },

            onEdit: function(event) {
                event.preventDefault();
                var self = this;
                this.$currentPost = $(event.target).closest('li');
                this.$currentPost.addClass('editing');

                $(this.editor(event)).show();
                var $textArea = this.$currentPost.find('.new-update-content').first();
                var targetModel = this.eventModel(event);
214
            // translate long-form date (for viewing) into short-form date (for input)
215 216 217 218 219 220 221
                if (targetModel.get('date') && targetModel.isValid()) {
                    $(this.dateEntry(event)).val($.datepicker.formatDate('mm/dd/yy', new Date(targetModel.get('date'))));
                }
                else {
                    $(this.dateEntry(event)).val('MM/DD/YY');
                }
                this.$codeMirror = CourseInfoHelper.editWithCodeMirror(
222
                targetModel, 'content', self.options.base_asset_url, $textArea.get(0));
223

224
            // Variable stored for unit test.
225
                this.$modalCover = ModalUtils.showModalCover(false,
226
                function() {
227
                    self.closeEditor(false);
228 229
                }
            );
230 231

            // Ensure validity is marked appropriately
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
                targetModel.isValid();
            },

            onDelete: function(event) {
                event.preventDefault();

                var self = this;
                var targetModel = this.eventModel(event);
                var confirm = new PromptView.Warning({
                    title: gettext('Are you sure you want to delete this update?'),
                    message: gettext('This action cannot be undone.'),
                    actions: {
                        primary: {
                            text: gettext('OK'),
                            click: function() {
                                analytics.track('Deleted Course Update', {
                                    'course': course_location_analytics,
                                    'date': self.dateEntry(event).val()
                                });
                                self.modelDom(event).remove();
                                var deleting = new NotificationView.Mini({
                                    title: gettext('Deleting')
                                });
                                deleting.show();
                                targetModel.destroy({
257
                                    success: function() {
258 259
                                        self.collection.fetch({
                                            success: function() {
260 261 262
                                                self.render();
                                                deleting.hide();
                                            },
263 264 265 266 267 268 269 270 271 272 273 274
                                            reset: true
                                        });
                                    }
                                });
                                confirm.hide();
                            }
                        },
                        secondary: {
                            text: gettext('Cancel'),
                            click: function() {
                                confirm.hide();
                            }
275 276
                        }
                    }
277 278 279
                });
                confirm.show();
            },
280

281
            closeEditor: function(removePost) {
282 283
                var content,
                    targetModel = this.collection.get(this.$currentPost.attr('name'));
284

285 286
            // If the model was never created (user created a new update, then pressed Cancel),
            // we wish to remove it from the DOM.
287 288
                if (removePost) {
                    this.$currentPost.remove();
289
                } else {
290
                // close the modal and insert the appropriate data
291
                    this.$currentPost.removeClass('editing');
292
                    this.$currentPost.find('.date-display').text(targetModel.get('date'));
293
                    this.$currentPost.find('.date').val(targetModel.get('date'));
294

295 296 297
                    content = HtmlUtils.HTML(CourseInfoHelper.changeContentToPreview(
                        targetModel, 'content', this.options.base_asset_url
                    ));
298
                    try {
299
                    // just in case the content causes an error (embedded js errors)
300
                        HtmlUtils.setHtml(this.$currentPost.find('.update-contents'), content);
301 302
                        this.$currentPost.find('.new-update-content').val(content);
                    } catch (e) {
303
                    // ignore but handle rest of page
304 305 306
                    }
                    this.$currentPost.find('form').hide();
                    this.$currentPost.find('.CodeMirror').remove();
307 308

                // hide the push notification checkbox for subsequent edits to the Post
309 310 311 312
                    var push_notification_ele = this.$currentPost.find('.new-update-push-notification');
                    if (push_notification_ele) {
                        push_notification_ele.hide();
                    }
313
                }
314

315 316 317
                ModalUtils.hideModalCover(this.$modalCover);
                this.$codeMirror = null;
            },
318 319

        // Dereferencing from events to screen elements
320
            eventModel: function(event) {
321
            // not sure if it should be currentTarget or delegateTarget
322 323
                return this.collection.get($(event.currentTarget).attr('name'));
            },
324

325 326 327
            modelDom: function(event) {
                return $(event.currentTarget).closest('li');
            },
328

329 330 331 332 333 334
            editor: function(event) {
                var li = $(event.currentTarget).closest('li');
                if (li) {
                    return $(li).find('form').first();
                }
            },
335

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
            dateEntry: function(event) {
                var li = $(event.currentTarget).closest('li');
                if (li) {
                    return $(li).find('.date').first();
                }
            },

            push_notification_selected: function(event) {
                var push_notification_checkbox;
                var li = $(event.currentTarget).closest('li');
                if (li) {
                    push_notification_checkbox = li.find('.new-update-push-notification .toggle-checkbox');
                    if (push_notification_checkbox) {
                        return push_notification_checkbox.is(':checked');
                    }
351 352
                }
            }
353
        });
354

355 356
        return CourseInfoUpdateView;
    }); // end define()