/////////////////////////////////////////////////////////////
//
// 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 '