fields.js 23.7 KB
Newer Older
Usman Khalid committed
1 2 3
;(function (define, undefined) {
    'use strict';
    define([
muhammad-ammar committed
4
        'gettext', 'jquery', 'underscore', 'backbone', 
5
        'edx-ui-toolkit/js/utils/html-utils',
muhammad-ammar committed
6 7 8 9 10
        'text!templates/fields/field_readonly.underscore',
        'text!templates/fields/field_dropdown.underscore',
        'text!templates/fields/field_link.underscore',
        'text!templates/fields/field_text.underscore',
        'text!templates/fields/field_textarea.underscore',
11
        'backbone-super'
12
    ], function (gettext, $, _, Backbone, HtmlUtils,
muhammad-ammar committed
13 14 15 16
                 field_readonly_template,
                 field_dropdown_template,
                 field_link_template,
                 field_text_template,
17
                 field_textarea_template
muhammad-ammar committed
18
    ) {
Usman Khalid committed
19

20
        var messageRevertDelay = 6000;
Usman Khalid committed
21 22 23
        var FieldViews = {};

        FieldViews.FieldView = Backbone.View.extend({
muzaffaryousaf committed
24
                
Usman Khalid committed
25 26 27 28 29 30 31 32 33
            fieldType: 'generic',

            className: function () {
                return 'u-field' + ' u-field-' + this.fieldType + ' u-field-' + this.options.valueAttribute;
            },

            tagName: 'div',

            indicators: {
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
                'canEdit': HtmlUtils.joinHtml(
                    HtmlUtils.HTML('<span class="icon fa fa-pencil message-can-edit" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
                    gettext("Editable"),
                    HtmlUtils.HTML('</span>')
                ),
                'error': HtmlUtils.joinHtml(
                    HtmlUtils.HTML('<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
                    gettext("Error"),
                    HtmlUtils.HTML('</span>')
                ),
                'validationError': HtmlUtils.joinHtml(
                    HtmlUtils.HTML('<span class="fa fa-exclamation-triangle message-validation-error" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
                    gettext("Validation Error"),
                    HtmlUtils.HTML('</span>')
                ),
                'inProgress': HtmlUtils.joinHtml(
                    HtmlUtils.HTML('<span class="fa fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
                    gettext("In Progress"),
                    HtmlUtils.HTML('</span>')
                ),
                'success': HtmlUtils.joinHtml(
                    HtmlUtils.HTML('<span class="fa fa-check message-success" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
                    gettext("Success"),
                    HtmlUtils.HTML('</span>')
                ),
                'plus': HtmlUtils.joinHtml(
                    HtmlUtils.HTML('<span class="fa fa-plus placeholder" aria-hidden="true"></span><span class="sr">'),
                    gettext("Placeholder"),
                    HtmlUtils.HTML('</span>')
63
                ),
Usman Khalid committed
64 65 66
            },

            messages: {
muzaffaryousaf committed
67
                'canEdit': '',
Usman Khalid committed
68 69 70 71 72 73
                'error': gettext('An error occurred. Please try again.'),
                'validationError': '',
                'inProgress': gettext('Saving'),
                'success': gettext('Your changes have been saved.')
            },

74 75 76 77 78
            constructor: function(options) {
                this.options = _.extend({}, options);
                Backbone.View.apply(this, arguments);
            },

79
            initialize: function () {
Usman Khalid committed
80

muhammad-ammar committed
81
                this.template = _.template(this.fieldTemplate || '');
Usman Khalid committed
82 83 84 85

                this.helpMessage = this.options.helpMessage || '';
                this.showMessages = _.isUndefined(this.options.showMessages) ? true : this.options.showMessages;

86 87 88 89
                _.bindAll(this, 'modelValue', 'modelValueIsSet', 'showNotificationMessage','getNotificationMessage',
                    'getMessage', 'title', 'showHelpMessage', 'showInProgressMessage', 'showSuccessMessage',
                    'showErrorMessage'
                );
Usman Khalid committed
90 91 92 93 94 95
            },

            modelValue: function () {
                return this.model.get(this.options.valueAttribute);
            },

muzaffaryousaf committed
96
            modelValueIsSet: function() {
97
                return (this.modelValue() === true);
Usman Khalid committed
98 99
            },

100 101
            title: function (title) {
                return HtmlUtils.setHtml(this.$('.u-field-title'), title);
muzaffaryousaf committed
102 103
            },

Usman Khalid committed
104 105 106 107
            getMessage: function(message_status) {
                if ((message_status + 'Message') in this) {
                    return this[message_status + 'Message'].call(this);
                } else if (this.showMessages) {
108
                    return HtmlUtils.joinHtml(this.indicators[message_status], this.messages[message_status]);
Usman Khalid committed
109 110 111 112
                }
                return this.indicators[message_status];
            },

113 114 115 116 117
            showHelpMessage: function (message) {
                if (_.isUndefined(message) || _.isNull(message)) {
                    message = this.helpMessage;
                }
                this.$('.u-field-message-notification').html('');
118
                HtmlUtils.setHtml(this.$('.u-field-message-help'), message);
119 120 121
            },

            getNotificationMessage: function() {
122
                return HtmlUtils.HTML(this.$('.u-field-message-notification').html());
123 124 125 126
            },

            showNotificationMessage: function(message) {
                this.$('.u-field-message-help').html('');
127
                HtmlUtils.setHtml(this.$('.u-field-message-notification'), message);
128 129
            },

muzaffaryousaf committed
130 131
            showCanEditMessage: function(show) {
                if (!_.isUndefined(show) && show) {
132
                    this.showNotificationMessage(this.getMessage('canEdit'));
muzaffaryousaf committed
133
                } else {
134
                    this.showNotificationMessage('');
muzaffaryousaf committed
135 136 137
                }
            },

Usman Khalid committed
138
            showInProgressMessage: function () {
139
                this.showNotificationMessage(this.getMessage('inProgress'));
Usman Khalid committed
140 141 142 143
            },

            showSuccessMessage: function () {
                var successMessage = this.getMessage('success');
144
                this.showNotificationMessage(successMessage);
Usman Khalid committed
145 146

                if (this.options.refreshPageOnSave) {
147
                    location.reload(true);
Usman Khalid committed
148 149 150 151 152 153 154 155
                }

                var view = this;

                var context = Date.now();
                this.lastSuccessMessageContext = context;

                setTimeout(function () {
156 157
                    if ((context === view.lastSuccessMessageContext) &&
                        (view.getNotificationMessage().toString() === successMessage.toString())) {
158 159 160 161 162
                        if (view.editable === 'toggle') {
                            view.showCanEditMessage(true);
                        } else {
                            view.showHelpMessage();
                        }
Usman Khalid committed
163 164 165 166 167 168 169
                    }
                }, messageRevertDelay);
            },

            showErrorMessage: function (xhr) {
                if (xhr.status === 400) {
                    try {
170
                        var errors = JSON.parse(xhr.responseText),
171 172
                            validationErrorMessage = errors.field_errors[this.options.valueAttribute].user_message,
                            message = HtmlUtils.joinHtml(this.indicators.validationError, validationErrorMessage);
173
                        this.showNotificationMessage(message);
Usman Khalid committed
174
                    } catch (error) {
175
                        this.showNotificationMessage(this.getMessage('error'));
Usman Khalid committed
176 177
                    }
                } else {
178
                    this.showNotificationMessage(this.getMessage('error'));
Usman Khalid committed
179 180 181 182
                }
            }
        });

muzaffaryousaf committed
183 184 185
        FieldViews.EditableFieldView = FieldViews.FieldView.extend({

            initialize: function (options) {
muhammad-ammar committed
186
                this.persistChanges = _.isUndefined(options.persistChanges) ? false : options.persistChanges;
187 188 189
                _.bindAll(this, 'saveAttributes', 'saveSucceeded', 'showDisplayMode', 'showEditMode',
                    'startEditing', 'finishEditing'
                );
muzaffaryousaf committed
190 191 192 193 194 195 196 197 198 199 200 201 202
                this._super(options);

                this.editable = _.isUndefined(this.options.editable) ? 'always': this.options.editable;
                this.$el.addClass('editable-' + this.editable);

                if (this.editable === 'always') {
                    this.showEditMode(false);
                } else {
                    this.showDisplayMode(false);
                }
            },

            saveAttributes: function (attributes, options) {
muhammad-ammar committed
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
                if (this.persistChanges === true) {
                    var view = this;
                    var defaultOptions = {
                        contentType: 'application/merge-patch+json',
                        patch: true,
                        wait: true,
                        success: function () {
                            view.saveSucceeded();
                        },
                        error: function (model, xhr) {
                            view.showErrorMessage(xhr);
                        }
                    };
                    this.showInProgressMessage();
                    this.model.save(attributes, _.extend(defaultOptions, options));
                }
muzaffaryousaf committed
219 220 221 222 223 224
            },

            saveSucceeded: function () {
                this.showSuccessMessage();
            },

225
            updateDisplayModeClass: function() {
muzaffaryousaf committed
226 227 228 229 230 231 232
                this.$el.removeClass('mode-edit');

                this.$el.toggleClass('mode-hidden', (this.editable === 'never' && !this.modelValueIsSet()));
                this.$el.toggleClass('mode-placeholder', (this.editable === 'toggle' && !this.modelValueIsSet()));
                this.$el.toggleClass('mode-display', (this.modelValueIsSet()));
            },

233 234 235 236 237 238
            showDisplayMode: function(render) {
                this.mode = 'display';
                if (render) { this.render(); }
                this.updateDisplayModeClass();
            },

muzaffaryousaf committed
239 240 241 242 243 244 245 246 247 248 249
            showEditMode: function(render) {
                this.mode = 'edit';
                if (render) { this.render(); }

                this.$el.removeClass('mode-hidden');
                this.$el.removeClass('mode-placeholder');
                this.$el.removeClass('mode-display');

                this.$el.addClass('mode-edit');
            },

250
            startEditing: function () {
muzaffaryousaf committed
251 252 253 254 255
                if (this.editable === 'toggle' && this.mode !== 'edit') {
                    this.showEditMode(true);
                }
            },

256
            finishEditing: function() {
257
                var modelValue;
258
                if (this.persistChanges === false || this.mode !== 'edit') {return;}
259 260 261 262 263 264 265

                modelValue = this.modelValue();
                if (!(_.isUndefined(modelValue) || _.isNull(modelValue))) {
                    modelValue = modelValue.toString();
                }

                if (this.fieldValue() !== modelValue) {
muzaffaryousaf committed
266 267 268 269 270 271 272 273
                    this.saveValue();
                } else {
                    if (this.editable === 'always') {
                        this.showEditMode(true);
                    } else {
                        this.showDisplayMode(true);
                    }
                }
muhammad-ammar committed
274 275 276 277 278 279 280 281
            },

            highlightFieldOnError: function () {
                this.$el.addClass('error');
            },

            unhighlightField: function () {
                this.$el.removeClass('error');
muzaffaryousaf committed
282 283 284
            }
        });

Usman Khalid committed
285 286 287 288
        FieldViews.ReadonlyFieldView = FieldViews.FieldView.extend({

            fieldType: 'readonly',

muhammad-ammar committed
289
            fieldTemplate: field_readonly_template,
Usman Khalid committed
290 291 292 293 294 295 296 297

            initialize: function (options) {
                this._super(options);
                _.bindAll(this, 'render', 'fieldValue', 'updateValueInField');
                this.listenTo(this.model, "change:" + this.options.valueAttribute, this.updateValueInField);
            },

            render: function () {
298
                HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
Usman Khalid committed
299 300
                    id: this.options.valueAttribute,
                    title: this.options.title,
301
                    screenReaderTitle: this.options.screenReaderTitle || this.options.title,
Usman Khalid committed
302 303 304
                    value: this.modelValue(),
                    message: this.helpMessage
                }));
305
                this.delegateEvents();
Usman Khalid committed
306 307 308 309
                return this;
            },

            fieldValue: function () {
310
                return this.$('.u-field-value').text();
Usman Khalid committed
311 312 313
            },

            updateValueInField: function () {
314
                this.$('.u-field-value ').text(this.modelValue());
Usman Khalid committed
315 316 317
            }
        });

muzaffaryousaf committed
318
        FieldViews.TextFieldView = FieldViews.EditableFieldView.extend({
Usman Khalid committed
319 320 321

            fieldType: 'text',

muhammad-ammar committed
322
            fieldTemplate: field_text_template,
Usman Khalid committed
323 324 325 326 327 328 329 330 331 332 333 334

            events: {
                'change input': 'saveValue'
            },

            initialize: function (options) {
                this._super(options);
                _.bindAll(this, 'render', 'fieldValue', 'updateValueInField', 'saveValue');
                this.listenTo(this.model, "change:" + this.options.valueAttribute, this.updateValueInField);
            },

            render: function () {
335
                HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
Usman Khalid committed
336 337 338 339 340
                    id: this.options.valueAttribute,
                    title: this.options.title,
                    value: this.modelValue(),
                    message: this.helpMessage
                }));
341
                this.delegateEvents();
Usman Khalid committed
342 343 344 345 346 347 348 349 350
                return this;
            },

            fieldValue: function () {
                return this.$('.u-field-value input').val();
            },

            updateValueInField: function () {
                var value = (_.isUndefined(this.modelValue()) || _.isNull(this.modelValue())) ? '' : this.modelValue();
351
                this.$('.u-field-value input').val(value);
Usman Khalid committed
352 353
            },

354
            saveValue: function () {
Usman Khalid committed
355 356 357 358 359 360
                var attributes = {};
                attributes[this.options.valueAttribute] = this.fieldValue();
                this.saveAttributes(attributes);
            }
        });

muzaffaryousaf committed
361
        FieldViews.DropdownFieldView = FieldViews.EditableFieldView.extend({
Usman Khalid committed
362 363 364

            fieldType: 'dropdown',

muhammad-ammar committed
365
            fieldTemplate: field_dropdown_template,
Usman Khalid committed
366 367

            events: {
muzaffaryousaf committed
368 369 370
                'click': 'startEditing',
                'change select': 'finishEditing',
                'focusout select': 'finishEditing'
Usman Khalid committed
371 372 373
            },

            initialize: function (options) {
muzaffaryousaf committed
374
                _.bindAll(this, 'render', 'optionForValue', 'fieldValue', 'displayValue', 'updateValueInField', 'saveValue');
Usman Khalid committed
375
                this._super(options);
muzaffaryousaf committed
376

Usman Khalid committed
377 378 379 380
                this.listenTo(this.model, "change:" + this.options.valueAttribute, this.updateValueInField);
            },

            render: function () {
381
                HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
Usman Khalid committed
382
                    id: this.options.valueAttribute,
muzaffaryousaf committed
383
                    mode: this.mode,
384
                    editable: this.editable,
Usman Khalid committed
385
                    title: this.options.title,
386
                    screenReaderTitle: this.options.screenReaderTitle || this.options.title,
387
                    titleVisible: this.options.titleVisible !== undefined ? this.options.titleVisible : true,
muzaffaryousaf committed
388
                    iconName: this.options.iconName,
389
                    showBlankOption: (!this.options.required || !this.modelValueIsSet()),
Usman Khalid committed
390
                    selectOptions: this.options.options,
391
                    message: this.helpMessage
Usman Khalid committed
392
                }));
393
                this.delegateEvents();
394
                this.updateValueInField();
muzaffaryousaf committed
395 396 397 398

                if (this.editable === 'toggle') {
                    this.showCanEditMessage(this.mode === 'display');
                }
Usman Khalid committed
399 400 401
                return this;
            },

muzaffaryousaf committed
402 403
            modelValueIsSet: function() {
                var value = this.modelValue();
404
                if (_.isUndefined(value) || _.isNull(value) || value === '') {
muzaffaryousaf committed
405 406
                    return false;
                } else {
407
                    return !(_.isUndefined(this.optionForValue(value)));
muzaffaryousaf committed
408 409 410 411
                }
            },

            optionForValue: function(value) {
412
                return _.find(this.options.options, function(option) { return option[0] === value; });
muzaffaryousaf committed
413 414
            },

Usman Khalid committed
415
            fieldValue: function () {
416 417 418 419 420 421 422
                var value;
                if (this.editable === 'never') {
                    value = this.modelValueIsSet() ? this.modelValue () : null;
                }
                else {
                    value = this.$('.u-field-value select').val();
                }
423
                return value === '' ? null : value;
Usman Khalid committed
424 425
            },

muzaffaryousaf committed
426 427 428 429 430 431 432 433 434
            displayValue: function (value) {
                if (value) {
                    var option = this.optionForValue(value);
                    return (option ? option[1] : '');
                } else {
                    return '';
                }
            },

Usman Khalid committed
435
            updateValueInField: function () {
436 437 438
                if (this.editable !== 'never') {
                    this.$('.u-field-value select').val(this.modelValue() || '');
                }
439 440 441 442 443 444

                var value = this.displayValue(this.modelValue() || '');
                if (this.modelValueIsSet() === false) {
                    value = this.options.placeholderValue || '';
                }
                this.$('.u-field-value').attr('aria-label', this.options.title);
445
                this.$('.u-field-value-readonly').text(value);
446

muzaffaryousaf committed
447
                if (this.mode === 'display') {
448
                    this.updateDisplayModeClass();
muzaffaryousaf committed
449 450 451 452 453 454 455 456 457
                }
            },

            saveValue: function () {
                var attributes = {};
                attributes[this.options.valueAttribute] = this.fieldValue();
                this.saveAttributes(attributes);
            },

458 459 460 461 462 463 464
            showDisplayMode: function(render) {
                this._super(render);
                if (this.editable === 'toggle') {
                    this.$('.u-field-value a').focus();
                }
            },

muzaffaryousaf committed
465 466 467 468 469 470 471 472 473
            showEditMode: function(render) {
                this._super(render);
                if (this.editable === 'toggle') {
                    this.$('.u-field-value select').focus();
                }
            },

            saveSucceeded: function() {
                if (this.editable === 'toggle') {
474 475 476 477 478
                    this.showDisplayMode();
                }

                if (this.options.required && this.modelValueIsSet()) {
                    this.$('option[value=""]').remove();
muzaffaryousaf committed
479
                }
480

481
                this._super();
482 483 484
            },

            disableField: function(disable) {
485 486 487
                if (this.editable !== 'never') {
                    this.$('.u-field-value select').prop('disabled', disable);
                }
muzaffaryousaf committed
488 489 490 491 492 493 494
            }
        });

        FieldViews.TextareaFieldView = FieldViews.EditableFieldView.extend({

            fieldType: 'textarea',

muhammad-ammar committed
495
            fieldTemplate: field_textarea_template,
muzaffaryousaf committed
496 497 498 499 500 501 502

            events: {
                'click .wrapper-u-field': 'startEditing',
                'click .u-field-placeholder': 'startEditing',
                'focusout textarea': 'finishEditing',
                'change textarea': 'adjustTextareaHeight',
                'keyup textarea': 'adjustTextareaHeight',
503
                'keydown textarea': 'onKeyDown',
muzaffaryousaf committed
504 505 506 507 508
                'paste textarea': 'adjustTextareaHeight',
                'cut textarea': 'adjustTextareaHeight'
            },

            initialize: function (options) {
509
                _.bindAll(this, 'render', 'onKeyDown', 'adjustTextareaHeight', 'fieldValue', 'saveValue', 'updateView');
muzaffaryousaf committed
510 511 512 513 514 515 516 517 518
                this._super(options);
                this.listenTo(this.model, "change:" + this.options.valueAttribute, this.updateView);
            },

            render: function () {
                var value = this.modelValue();
                if (this.mode === 'display') {
                    value = value || this.options.placeholderValue;
                }
519
                HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
muzaffaryousaf committed
520
                    id: this.options.valueAttribute,
521
                    screenReaderTitle: this.options.screenReaderTitle || this.options.title,
muzaffaryousaf committed
522
                    mode: this.mode,
523
                    editable: this.editable,
muzaffaryousaf committed
524
                    value: value,
525
                    message: this.helpMessage,
muhammad-ammar committed
526
                    messagePosition: this.options.messagePosition || 'footer',
527
                    placeholderValue: this.options.placeholderValue
muzaffaryousaf committed
528
                }));
529
                this.delegateEvents();
530 531
                this.title((this.modelValue() || this.mode === 'edit') ?
                    this.options.title : HtmlUtils.joinHtml(this.indicators.plus, this.options.title));
muzaffaryousaf committed
532 533 534 535 536 537 538

                if (this.editable === 'toggle') {
                    this.showCanEditMessage(this.mode === 'display');
                }
                return this;
            },

539 540 541 542 543 544 545 546 547
            onKeyDown: function (event) {
                if (event.keyCode === 13) {
                    event.preventDefault();
                    this.finishEditing(event);
                } else {
                    this.adjustTextareaHeight();
                }
            },

548
            adjustTextareaHeight: function() {
muhammad-ammar committed
549
                if (this.persistChanges === false) {return;}
muzaffaryousaf committed
550 551 552 553 554 555
                var textarea = this.$('textarea');
                textarea.css('height', 'auto').css('height', textarea.prop('scrollHeight') + 10);
            },

            modelValue: function() {
                var value = this._super();
556
                return value ? $.trim(value) : '';
muzaffaryousaf committed
557 558 559
            },

            fieldValue: function () {
560 561 562 563 564 565
                if (this.mode === 'edit') {
                    return this.$('.u-field-value textarea').val();
                }
                else {
                    return this.$('.u-field-value .u-field-value-readonly').text();
                }
Usman Khalid committed
566 567 568 569 570 571
            },

            saveValue: function () {
                var attributes = {};
                attributes[this.options.valueAttribute] = this.fieldValue();
                this.saveAttributes(attributes);
muzaffaryousaf committed
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
            },

            updateView: function () {
                if (this.mode !== 'edit') {
                    this.showDisplayMode(true);
                }
            },

            modelValueIsSet: function() {
                return !(this.modelValue() === '');
            },

            showEditMode: function(render) {
                this._super(render);
                this.adjustTextareaHeight();
                this.$('.u-field-value textarea').focus();
            },

            saveSucceeded: function() {
                this._super();
                if (this.editable === 'toggle') {
                    this.showDisplayMode(true);
594
                    this.$('a').focus();
muzaffaryousaf committed
595
                }
Usman Khalid committed
596 597 598 599 600 601 602
            }
        });

        FieldViews.LinkFieldView = FieldViews.FieldView.extend({

            fieldType: 'link',

muhammad-ammar committed
603
            fieldTemplate: field_link_template,
Usman Khalid committed
604 605 606 607 608 609 610 611 612 613 614

            events: {
                'click a': 'linkClicked'
            },

            initialize: function (options) {
                this._super(options);
                _.bindAll(this, 'render', 'linkClicked');
            },

            render: function () {
615
                HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
Usman Khalid committed
616 617
                    id: this.options.valueAttribute,
                    title: this.options.title,
618
                    screenReaderTitle: this.options.screenReaderTitle || this.options.title,
Usman Khalid committed
619 620
                    linkTitle: this.options.linkTitle,
                    linkHref: this.options.linkHref,
Usman Khalid committed
621
                    message: this.helpMessage
Usman Khalid committed
622
                }));
623
                this.delegateEvents();
Usman Khalid committed
624 625 626
                return this;
            },

627
            linkClicked: function (event) {
Usman Khalid committed
628 629 630 631 632
                event.preventDefault();
            }
        });

        return FieldViews;
633
    });
Usman Khalid committed
634
}).call(this, define || RequireJS.define);