///////////////////////////////////////////////////////////// // // pgAdmin 4 - PostgreSQL Tools // // Copyright (C) 2013 - 2022, The pgAdmin Development Team // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// import Notify from '../../static/js/helpers/Notifier'; define([ 'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify', 'moment', 'bignumber', 'codemirror', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll', 'sources/window', 'sources/url_for', 'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle', ], function( gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber, CodeMirror, commonUtils, keyboardShortcuts, configure_show_on_scroll, pgWindow, url_for ) { /* * Add mechanism in backgrid to render different types of cells in * same column; */ let pgAdmin = (window.pgAdmin = window.pgAdmin || {}), pgBrowser = pgAdmin.Browser; // Add new property cellFunction in Backgrid.Column. _.extend(Backgrid.Column.prototype.defaults, { cellFunction: undefined, }); // Add tooltip to cell if cell content is larger than // cell width _.extend(Backgrid.Cell.prototype.events, { 'mouseover': function() { var $el = $(this.el); if ($el.text().length > 0 && !$el.attr('title') && ($el.innerWidth() + 1) < $el[0].scrollWidth ) { $el.attr('title', $.trim($el.text())); } }, }); // bind shortcut in cell edit mode _.extend(Backgrid.InputCellEditor.prototype.events, { 'keydown': function(e) { let preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('browser'); if(preferences && keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) { pgBrowser.keyboardNavigation.bindAddGridRow(); } else { Backgrid.InputCellEditor.prototype.saveOrCancel.apply(this, arguments); } }, }); /* Overriding backgrid sort method. * As we are getting numeric, integer values as string * from server side, but on client side javascript truncates * large numbers automatically due to which backgrid was unable * to sort numeric values properly in the grid. * To fix this issue, now we check if cell type is integer/number * convert it into BigNumber object and make comparison to perform sorting. */ _.extend(Backgrid.Body.prototype, { sort: function(column, direction) { if (!_.contains(['ascending', 'descending', null], direction)) { throw new RangeError('direction must be one of "ascending", "descending" or `null`'); } if (_.isString(column)) column = this.columns.findWhere({ name: column, }); var collection = this.collection; var order; if (direction === 'ascending') order = -1; else if (direction === 'descending') order = 1; else order = null; // Get column type and pass it to comparator. var col_type = column.get('cell').prototype.className || 'string-cell', comparator = this.makeComparator(column.get('name'), order, order ? column.sortValue() : function(model) { return model.cid.replace('c', '') * 1; }, col_type); if (Backbone.PageableCollection && collection instanceof Backbone.PageableCollection) { collection.setSorting(order && column.get('name'), order, { sortValue: column.sortValue(), }); if (collection.fullCollection) { // If order is null, pageable will remove the comparator on both sides, // in this case the default insertion order comparator needs to be // attached to get back to the order before sorting. if (collection.fullCollection.comparator == null) { collection.fullCollection.comparator = comparator; } collection.fullCollection.sort(); collection.trigger('backgrid:sorted', column, direction, collection); } else collection.fetch({ reset: true, success: function() { collection.trigger('backgrid:sorted', column, direction, collection); }, }); } else { collection.comparator = comparator; collection.sort(); collection.trigger('backgrid:sorted', column, direction, collection); } column.set('direction', direction); return this; }, makeComparator: function(attr, order, func, type) { return function(left, right) { // extract the values from the models var l = func(left, attr), r = func(right, attr), t; if (_.isUndefined(l) || _.isUndefined(r)) return; var types = ['number-cell', 'integer-cell']; if (_.include(types, type)) { var _l, _r; // NaN if invalid number try { _l = new BigNumber(l); } catch (err) { _l = NaN; } try { _r = new BigNumber(r); } catch (err) { _r = NaN; } // if descending order, swap left and right if (order === 1) { t = _l; _l = _r; _r = t; } if (_l.eq(_r)) // If both are equals return 0; else if (_l.lt(_r)) // If left is less than right return -1; else return 1; } else { // if descending order, swap left and right if (order === 1) { t = l; l = r; r = t; } // compare as usual if (l === r) return 0; else if (l === null && r != null) return -1; else if (l != null && r === null) return 1; else if (l < r) return -1; return 1; } }; }, moveToNextCell: function (model, column, command) { var i = this.collection.indexOf(model); var j = this.columns.indexOf(column); var cell, renderable, editable, m, n; // return if model being edited in a different grid if (j === -1) return this; this.rows[i].cells[j].exitEditMode(); if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || command.save()) { var l = this.columns.length; var maxOffset = l * this.collection.length; if (command.moveUp() || command.moveDown()) { m = i + (command.moveUp() ? -1 : 1); var row = this.rows[m]; if (row) { cell = row.cells[j]; if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) { cell.enterEditMode(); model.trigger('backgrid:next', m, j, false); } } else model.trigger('backgrid:next', m, j, true); } else if (command.moveLeft() || command.moveRight()) { var right = command.moveRight(); for (var offset = i * l + j + (right ? 1 : -1); offset >= 0 && offset < maxOffset; right ? offset++ : offset--) { m = ~~(offset / l); n = offset - m * l; cell = this.rows[m].cells[n]; renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model); editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model); if(cell && cell.$el.hasClass('edit-cell') && !cell.$el.hasClass('privileges') || cell.$el.hasClass('delete-cell')) { model.trigger('backgrid:next', m, n, false); if(cell.$el.hasClass('delete-cell')) { setTimeout(function(){ $(cell.$el).trigger('focus'); }, 50); } break; } else if (renderable && editable) { cell.enterEditMode(); model.trigger('backgrid:next', m, n, false); break; } } if (offset == maxOffset) { model.trigger('backgrid:next', ~~(offset / l), offset - m * l, true); } } } return this; }, }); _.extend(Backgrid.Row.prototype, { makeCell: function(column) { return new(this.getCell(column))({ column: column, model: this.model, }); }, /* * getCell function will check and execute user given cellFunction to get * appropriate cell class for current cell being rendered. * User provided cellFunction must return valid cell class. * cellFunction will be called with context (this) as column and model as * argument. */ getCell: function(column) { var cf = column.get('cellFunction'); if (_.isFunction(cf)) { var cell = cf.apply(column, [this.model]); try { return Backgrid.resolveNameToClass(cell, 'Cell'); } catch (e) { if (e instanceof ReferenceError) { // Fallback to column cell. return column.get('cell'); } else { throw e; // Let other exceptions bubble up } } } else { return column.get('cell'); } }, }); var ObjectCellEditor = Backgrid.Extension.ObjectCellEditor = Backgrid.CellEditor.extend({ modalTemplate: _.template([ '
', '
', '
', ].join('\n')), stringTemplate: _.template([ '
', ' ', '
', ' ', '
', '
', ].join('\n')), extendWithOptions: function(options) { _.extend(this, options); }, render: function() { return this; }, postRender: function(model, column) { var columns_length = this.columns_length, // To render schema directly from Backgrid cell we use columns schema // attribute. schema = this.schema.length ? this.schema : this.column.get('schema'); if (column != null && column.get('name') != this.column.get('name')) return false; if (!_.isArray(schema)) throw new TypeError('schema must be an array'); // Create a Backbone model from our object if it does not exist var $dialog = this.createDialog(columns_length); // Add the Bootstrap form var $form = $('
'); $dialog.find('div.subnode-body').append($form); // Call Backform to prepare dialog var back_el = $dialog.find('form.form-dialog'); this.objectView = new Backform.Dialog({ el: back_el, model: this.model, schema: schema, tabPanelClassName: function() { return 'sub-node-form col-sm-12'; }, events: { 'keydown': function (event) { let preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('browser'); if(preferences && keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,event)) { pgBrowser.keyboardNavigation.bindAddGridRow(); } }, }, }); this.objectView.render(); return this; }, createDialog: function(noofcol) { noofcol = noofcol || 1; var $dialog = this.$dialog = $(this.modalTemplate({ title: '', })), tr = $(''), td = $('', { class: 'editable sortable renderable', style: 'height: auto', colspan: noofcol + 2, }).appendTo(tr); this.tr = tr; // Show the Bootstrap modal dialog td.append($dialog.css('display', 'block')); this.el.parent('tr').after(tr); return $dialog; }, save: function() { // Retrieve values from the form, and store inside the object model this.model.trigger('backgrid:edited', this.model, this.column, new Backgrid.Command({ keyCode: 13, })); if (this.tr) { this.tr.remove(); } return this; }, remove: function() { this.objectView.remove(); Backgrid.CellEditor.prototype.remove.apply(this, arguments); if (this.tr) { this.tr.remove(); } return this; }, }); Backgrid.Extension.PGSelectCell = Backgrid.SelectCell.extend({ // It's possible to render an option group or use a // function to provide option values too. optionValues: function() { var res = [], opts = _.result(this.column.attributes, 'options'); _.each(opts, function(o) { res.push([o.label, o.value]); }); return res; }, }); Backgrid.Extension.ObjectCell = Backgrid.Cell.extend({ editorOptionDefaults: { schema: [], }, className: 'edit-cell', editor: ObjectCellEditor, initialize: function(options) { Backgrid.Cell.prototype.initialize.apply(this, arguments); // Pass on cell options to the editor var cell = this, editorOptions = {}; _.each(this.editorOptionDefaults, function(def, opt) { if (!cell[opt]) cell[opt] = def; if (options && options[opt]) cell[opt] = options[opt]; editorOptions[opt] = cell[opt]; }); editorOptions['el'] = $(this.el); editorOptions['columns_length'] = this.column.collection.length; editorOptions['el'].attr('tabindex', 0); this.listenTo(this.model, 'backgrid:edit', function(model, column, sel_cell, editor) { if (column.get('name') == this.column.get('name')) editor.extendWithOptions(editorOptions); }); // Listen for Tab key, open subnode dialog on space key this.$el.on('keydown', function(e) { if (e.keyCode == 32) { $(this).click(); } }); }, enterEditMode: function() { // Notify that we are about to enter in edit mode for current cell. // We will check if this row is editable first var canEditRow = (!_.isUndefined(this.column.get('canEditRow')) && _.isFunction(this.column.get('canEditRow'))) ? Backgrid.callByNeed(this.column.get('canEditRow'), this.column, this.model) : true; if (canEditRow) { // Notify that we are about to enter in edit mode for current cell. this.model.trigger('enteringEditMode', [this]); Backgrid.Cell.prototype.enterEditMode.apply(this, arguments); /* Make sure - we listen to the click event */ this.delegateEvents(); var editable = Backgrid.callByNeed(this.column.editable(), this.column, this.model); if (editable) { this.$el.html( '' ); let body = $(this.$el).parents()[1], container = $(body).find('.tab-content:first > .tab-pane.active:first'); commonUtils.findAndSetFocus(container); pgBrowser.keyboardNavigation.getDialogTabNavigator($(body).find('.subnode-dialog')); this.model.trigger( 'pg-sub-node:opened', this.model, this ); } } else { Notify.alert(gettext('Edit object'), gettext('This object is not user editable.')); } }, render: function() { this.$el.empty(); this.$el.html(''); this.delegateEvents(); if (this.grabFocus) this.$el.trigger('focus'); return this; }, exitEditMode: function() { if(!_.isUndefined(this.currentEditor) || !_.isEmpty(this.currentEditor)) { var index = $(this.currentEditor.objectView.el) .find('.nav-tabs > .active > a[data-toggle="tab"]').first() .data('tabIndex'); Backgrid.Cell.prototype.exitEditMode.apply(this, arguments); this.model.trigger( 'pg-sub-node:closed', this, index ); this.grabFocus = true; } }, events: { 'click': function(e) { if (this.$el.find('i').first().hasClass('subnode-edit-in-process')) { // Need to redundantly undelegate events for Firefox this.undelegateEvents(); this.currentEditor.save(); } else { this.enterEditMode.call(this, []); } e.preventDefault(); }, 'keydown': function(e) { var model = this.model; var column = this.column; var command = new Backgrid.Command(e); if (command.moveLeft()) { setTimeout(function() { model.trigger('backgrid:edited', model, column, command); }, 20); } }, }, }); Backgrid.Extension.DeleteCell = Backgrid.Cell.extend({ defaults: _.defaults({ defaultDeleteMsg: gettext('Are you sure you wish to delete this row?'), defaultDeleteTitle: gettext('Delete Row'), }, Backgrid.Cell.prototype.defaults), /** @property */ className: 'delete-cell', events: { 'click': 'deleteRow', }, deleteRow: function(e) { e.preventDefault(); var that = this; // We will check if row is deletable or not var canDeleteRow = (!_.isUndefined(this.column.get('canDeleteRow')) && _.isFunction(this.column.get('canDeleteRow'))) ? Backgrid.callByNeed(this.column.get('canDeleteRow'), this.column, this.model) : true; if (canDeleteRow) { var delete_msg = !_.isUndefined(this.column.get('customDeleteMsg')) ? this.column.get('customDeleteMsg') : that.defaults.defaultDeleteMsg; var delete_title = !_.isUndefined(this.column.get('customDeleteTitle')) ? this.column.get('customDeleteTitle') : that.defaults.defaultDeleteTitle; Notify.confirm( delete_title, delete_msg, function() { let tbody = $(that.el).parents('tbody').eq(0); that.model.collection.remove(that.model); let row = $(tbody).find('tr'); if(row.length > 0) { // set focus to first tr row.first().children()[0].focus(); } else { // set focus to add button $(tbody).parents('.subnode').eq(0).find('.add').focus(); } }, function() { return true; } ); } else { Notify.alert(gettext('Delete object'), gettext('This object cannot be deleted.')); } }, exitEditMode: function() { this.$el.removeClass('editor'); }, initialize: function() { Backgrid.Cell.prototype.initialize.apply(this, arguments); }, render: function() { var self = this; this.$el.empty(); $(this.$el).attr('tabindex', 0); this.$el.html(''); // Listen for Tab/Shift-Tab key this.$el.on('keydown', function(e) { // with keyboard navigation on space key, mark row for deletion if (e.keyCode == 32) { self.$el.click(); } var gotoCell; if (e.keyCode == 9 || e.keyCode == 16) { // go to Next Cell & if Shift is also pressed go to Previous Cell gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next(); } if (gotoCell) { let command = new Backgrid.Command({ key: 'Tab', keyCode: 9, which: 9, shiftKey: e.shiftKey, }); setTimeout(function() { // When we have Editable Cell if (gotoCell.hasClass('editable')) { e.preventDefault(); e.stopPropagation(); self.model.trigger('backgrid:edited', self.model, self.column, command); } else { // When we have Non-Editable Cell self.model.trigger('backgrid:edited', self.model, self.column, command); } }, 20); } }); this.delegateEvents(); return this; }, }); Backgrid.Extension.ClearCell = Backgrid.Cell.extend({ defaults: _.defaults({ defaultClearMsg: gettext('Are you sure you wish to clear this row?'), defaultClearTitle: gettext('Clear Row'), }, Backgrid.Cell.prototype.defaults), /** @property */ className: 'clear-cell', events: { 'click': 'clearRow', }, clearRow: function(e) { e.preventDefault(); if (_.isEmpty(e.currentTarget.innerHTML)) return false; var that = this; // We will check if row is deletable or not var clear_msg = !_.isUndefined(this.column.get('customClearMsg')) ? this.column.get('customClearMsg') : that.defaults.defaultClearMsg; var clear_title = !_.isUndefined(this.column.get('customClearTitle')) ? this.column.get('customClearTitle') : that.defaults.defaultClearTitle; Notify.confirm( clear_title, clear_msg, function() { that.model.set('name', null); that.model.set('sql', null); }, function() { return true; } ); }, exitEditMode: function() { this.$el.removeClass('editor'); }, initialize: function() { Backgrid.Cell.prototype.initialize.apply(this, arguments); }, render: function() { var self = this; this.$el.empty(); $(this.$el).attr('tabindex', 0); if (this.model.get('name') !== null && this.model.get('sql') !== null) this.$el.html(''); // Listen for Tab/Shift-Tab key this.$el.on('keydown', function(e) { // with keyboard navigation on space key, mark row for deletion if (e.keyCode == 32) { self.$el.click(); } var gotoCell; if (e.keyCode == 9 || e.keyCode == 16) { // go to Next Cell & if Shift is also pressed go to Previous Cell gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next(); } if (gotoCell) { let command = new Backgrid.Command({ key: 'Tab', keyCode: 9, which: 9, shiftKey: e.shiftKey, }); setTimeout(function() { // When we have Editable Cell if (gotoCell.hasClass('editable')) { e.preventDefault(); e.stopPropagation(); self.model.trigger('backgrid:edited', self.model, self.column, command); } else { // When we have Non-Editable Cell self.model.trigger('backgrid:edited', self.model, self.column, command); } }, 20); } }); this.delegateEvents(); return this; }, }); Backgrid.Extension.CustomHeaderCell = Backgrid.HeaderCell.extend({ initialize: function() { // Here, we will add custom classes to header cell Backgrid.HeaderCell.prototype.initialize.apply(this, arguments); var getClassName = this.column.get('cellHeaderClasses'); var getAriaLabel = this.column.get('cellAriaLabel'); if (getClassName) { this.$el.addClass(getClassName); } if (getAriaLabel) { this.$el.attr('aria-label', getAriaLabel); } }, render: function() { Backgrid.HeaderCell.prototype.render.apply(this, arguments); // If table header label is not present then screen reader will raise // an error we will add span for screen reader only if (this.column.get('label') == '' || !this.column.get('label')) { let getAriaLabel = this.column.get('cellAriaLabel'); if (getAriaLabel) this.$el.append(`${getAriaLabel}`); } return this; }, }); /** SwitchCell renders a Bootstrap Switch in backgrid cell */ if (window.jQuery && window.jQuery.fn.bootstrapToggle) $.fn.bootstrapToggle = window.jQuery.fn.bootstrapToggle; Backgrid.Extension.SwitchCell = Backgrid.BooleanCell.extend({ defaults: { options: _.defaults({ onText: gettext('Yes'), offText: gettext('No'), onColor: 'success', offColor: 'ternary', size: 'mini', width: null, height: null, }, $.fn.bootstrapToggle.defaults), }, className: 'switch-cell', initialize: function() { Backgrid.BooleanCell.prototype.initialize.apply(this, arguments); this.onChange = this.onChange.bind(this); }, enterEditMode: function() { this.$el.addClass('editor'); $(this.$el.find('.toggle.btn')).trigger('focus'); }, exitEditMode: function() { this.$el.removeClass('editor'); }, events: { 'change input': 'onChange', 'blur input': 'exitEditMode', 'keydown': 'onKeyDown', }, onKeyDown: function(e) { let preferences = pgWindow.default.pgAdmin.Browser.get_preferences_for_module('browser'); if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) { pgBrowser.keyboardNavigation.bindAddGridRow(); } }, onChange: function() { var model = this.model, column = this.column, val = this.formatter.toRaw(this.$input.prop('checked'), model); this.enterEditMode(); // on bootstrap change we also need to change model's value model.set(column.get('name'), val); this.setSrValue(); }, setSrValue: function() { let {onText, offText} = _.defaults({}, this.column.get('options'), this.defaults.options); if(this.$el.find('.toggle.btn').hasClass('off')) { this.$el.find('.sr-value').text(` ${offText}, ${gettext('Toggle button')} `); } else { this.$el.find('.sr-value').text(` ${onText}, ${gettext('Toggle button')} `); } }, render: function() { var self = this, col = _.defaults(this.column.toJSON(), this.defaults), model = this.model, column = this.column, rawValue = this.formatter.fromRaw( model.get(column.get('name')), model ), editable = Backgrid.callByNeed(col.editable, column, model), options = _.defaults({}, col.options, this.defaults.options), cId = _.uniqueId('pgC_'); this.undelegateEvents(); this.$el.empty(); this.$el.append(''); this.$el.append( $('', { tabIndex: -1, type: 'checkbox', 'aria-hidden': 'true', 'aria-label': column.get('name'), }).prop('checked', rawValue).prop('disabled', !editable).attr('data-toggle', 'toggle') .attr('data-size', options.size).attr('data-on', options.onText).attr('data-off', options.offText) .attr('data-width', options.width).attr('data-height', options.height) .attr('data-onstyle', options.onColor).attr('data-offstyle', options.offColor)); this.$input = this.$el.find('input[type=checkbox]').first(); // Override BooleanCell checkbox with Bootstraptoggle this.$input.bootstrapToggle(); this.$el.find('.toggle.btn') .attr('tabindex', !editable ? '-1' : '0') .attr('id', cId) .on('keydown', function(e) { if (e.keyCode == 32) { self.$el.find('input[type=checkbox]').bootstrapToggle('toggle'); e.preventDefault(); e.stopPropagation(); self.setSrValue(); } }); this.$el.find('.toggle.btn .toggle-group .btn').attr('aria-hidden', true); this.setSrValue(); // Listen for Tab key this.$el.on('keydown', function(e) { var gotoCell; if (e.keyCode == 9) { // go to Next Cell & if Shift is also pressed go to Previous Cell gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next(); } if (gotoCell && gotoCell.length > 0) { if(gotoCell.hasClass('editable')){ e.preventDefault(); e.stopPropagation(); } let command = new Backgrid.Command({ key: 'Tab', keyCode: 9, which: 9, shiftKey: e.shiftKey, }); setTimeout(function() { // When we have Editable Cell if (gotoCell.hasClass('editable') && gotoCell.hasClass('edit-cell')) { gotoCell.trigger('focus'); } else if (gotoCell.hasClass('editable')) { setTimeout(function() { self.model.trigger('backgrid:edited', self.model, self.column, command); }, 10); gotoCell.trigger('focus'); } else { // When we have Non-Editable Cell setTimeout(function() { self.model.trigger('backgrid:edited', self.model, self.column, command); }, 10); } }, 20); } }); this.delegateEvents(); return this; }, }); /* * Select2Cell for backgrid. */ Backgrid.Extension.Select2Cell = Backgrid.SelectCell.extend({ className: 'select2-cell', /** @property */ editor: null, defaults: _.defaults({ select2: {}, opt: { label: null, value: null, selected: false, }, }, Backgrid.SelectCell.prototype.defaults), enterEditMode: function() { if (!this.$el.hasClass('editor')) this.$el.addClass('editor'); this.$select.select2('focus'); this.$select.select2('open'); this.$select.on('blur', this.exitEditMode); }, exitEditMode: function() { this.$select.off('blur', this.exitEditMode); this.$select.select2('close'); this.$el.removeClass('editor'); this.$el.find('.select2-selection').trigger('focus'); }, saveOrCancel: function (e) { var self = this; var command = new Backgrid.Command(e); var blurred = e.type === 'blur'; if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || command.save() || blurred) { let gotoCell; // go to Next Cell & if Shift is also pressed go to Previous Cell if (e.keyCode == 9 || e.keyCode == 16) { gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next(); if (self.$el.next().length == 0){ setTimeout(function() { self.$el.find('.select2-selection').blur(); }, 100); } } if (gotoCell) { let cmd = new Backgrid.Command({ key: 'Tab', keyCode: 9, which: 9, shiftKey: e.shiftKey, }); setTimeout(function() { // When we have Editable Cell if (gotoCell.hasClass('editable')) { e.preventDefault(); e.stopPropagation(); self.model.trigger('backgrid:edited', self.model, self.column, cmd); } else { // When we have Non-Editable Cell self.model.trigger('backgrid:edited', self.model, self.column, cmd); } }, 20); } } }, events: { 'select2:open': 'enterEditMode', 'select2:close': 'exitEditMode', 'change': 'onSave', 'select2:unselect': 'onSave', 'blur': 'saveOrCancel', 'keydown': 'saveOrCancel', }, /** @property {function(Object, ?Object=): string} template */ template: _.template([ '', ].join(''), null, { variable: null, }), initialize: function() { Backgrid.SelectCell.prototype.initialize.apply(this, arguments); this.onSave = this.onSave.bind(this); this.enterEditMode = this.enterEditMode.bind(this); this.exitEditMode = this.exitEditMode.bind(this); }, render: function() { var col = _.defaults(this.column.toJSON(), this.defaults), model = this.model, column = this.column, editable = Backgrid.callByNeed(col.editable, column, model), optionValues = _.clone(this.optionValues || (_.isFunction(this.column.get('options')) ? (this.column.get('options'))(this) : this.column.get('options'))); this.undelegateEvents(); if (this.$select) { if (this.$select.data('select2')) { this.$select.select2('destroy'); } delete this.$select; this.$select = null; } this.$el.empty(); if (!_.isArray(optionValues)) throw new TypeError('optionValues must be an array'); var optionText = null, optionValue = null, self = this, selectedValues = model.get(this.column.get('name')), select2_opts = _.extend({ openOnEnter: false, multiple: false, showOnScroll: true, first_empty: true, }, self.defaults.select2, (col.select2 || {}) ), selectTpl = _.template(''); var $select = self.$select = $(selectTpl({ multiple: select2_opts.multiple, })).appendTo(self.$el); /* * Add empty option as Select2 requires any empty '