491 lines
14 KiB
JavaScript
491 lines
14 KiB
JavaScript
|
var CoreView = require('backbone/core-view');
|
||
|
var $ = require('jquery');
|
||
|
var _ = require('underscore');
|
||
|
var Clipboard = require('clipboard');
|
||
|
var TableBodyRowView = require('./table-body-row-view');
|
||
|
var ContextMenuView = require('builder/components/context-menu/context-menu-view');
|
||
|
var CustomListCollection = require('builder/components/custom-list/custom-list-collection');
|
||
|
var addTableRowOperation = require('builder/components/table/operations/table-add-row');
|
||
|
var removeTableRowOperation = require('builder/components/table/operations/table-remove-row');
|
||
|
var editCellOperation = require('builder/components/table/operations/table-edit-cell');
|
||
|
var ConfirmationModalView = require('builder/components/modals/confirmation/modal-confirmation-view');
|
||
|
var TablePaginatorView = require('builder/components/table/paginator/table-paginator-view');
|
||
|
var tableBodyTemplate = require('./table-body.tpl');
|
||
|
var renderLoading = require('builder/components/loading/render-loading');
|
||
|
var ErrorView = require('builder/components/error/error-view');
|
||
|
var tableNoRowsTemplate = require('./table-no-rows.tpl');
|
||
|
var EditorsServiceModel = require('builder/components/table/editors/editors-service-model');
|
||
|
var EditorsModel = require('builder/components/table/editors/types/editor-model');
|
||
|
var errorParser = require('builder/helpers/error-parser');
|
||
|
var magicPositioner = require('builder/helpers/magic-positioner');
|
||
|
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||
|
|
||
|
var EDITORS_MAP = {
|
||
|
'string': require('builder/components/table/editors/types/editor-string-view'),
|
||
|
'number': require('builder/components/table/editors/types/editor-base-view'),
|
||
|
'boolean': require('builder/components/table/editors/types/editor-boolean-view'),
|
||
|
'date': require('builder/components/table/editors/types/editor-date-view'),
|
||
|
'default': require('builder/components/table/editors/types/editor-string-view')
|
||
|
};
|
||
|
|
||
|
var REQUIRED_OPTS = [
|
||
|
'columnsCollection',
|
||
|
'modals',
|
||
|
'queryGeometryModel',
|
||
|
'querySchemaModel',
|
||
|
'rowsCollection',
|
||
|
'canHideColumns',
|
||
|
'tableViewModel'
|
||
|
];
|
||
|
|
||
|
/*
|
||
|
* Table body view
|
||
|
*/
|
||
|
|
||
|
module.exports = CoreView.extend({
|
||
|
|
||
|
className: 'Table-body',
|
||
|
tagName: 'div',
|
||
|
|
||
|
events: {
|
||
|
'click': '_onClick',
|
||
|
'dblclick': '_onDblClick'
|
||
|
},
|
||
|
|
||
|
initialize: function (opts) {
|
||
|
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||
|
|
||
|
this._editors = new EditorsServiceModel();
|
||
|
|
||
|
this._closeEditor = this._closeEditor.bind(this);
|
||
|
this._hideContextMenu = this._hideContextMenu.bind(this);
|
||
|
|
||
|
this._initBinds();
|
||
|
},
|
||
|
|
||
|
render: function () {
|
||
|
this.clearSubViews();
|
||
|
this._destroyScrollBinding();
|
||
|
this.$el.empty();
|
||
|
|
||
|
// Render results when we have the schema and goemetry is not being fetched
|
||
|
if (this._querySchemaModel.isFetched() && !this._queryGeometryModel.isFetching() && !this._rowsCollection.isFetching()) {
|
||
|
if (!this._rowsCollection.size()) {
|
||
|
this._renderNoRows();
|
||
|
} else {
|
||
|
this.$el.html(tableBodyTemplate());
|
||
|
this._rowsCollection.each(this._renderBodyRow, this);
|
||
|
this._initPaginator();
|
||
|
}
|
||
|
} else {
|
||
|
this._renderQueryState();
|
||
|
}
|
||
|
|
||
|
this.$el.toggleClass('Table-body--relative', !!this._tableViewModel.get('relativePositionated'));
|
||
|
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
_initBinds: function () {
|
||
|
this._queryGeometryModel.bind('change:status', this.render, this);
|
||
|
this._querySchemaModel.bind('change:status', this.render, this);
|
||
|
this._rowsCollection.bind('reset', _.debounce(this.render.bind(this), 20), this);
|
||
|
this._rowsCollection.bind('add', function (model) {
|
||
|
if (this._rowsCollection.size() === 1) {
|
||
|
this.render();
|
||
|
} else {
|
||
|
this._renderBodyRow(model);
|
||
|
}
|
||
|
}, this);
|
||
|
this._rowsCollection.bind('remove', this._onRemoveRow, this);
|
||
|
this._rowsCollection.bind('fail', function (mdl, response) {
|
||
|
if (!response || (response && response.statusText !== 'abort')) {
|
||
|
this._renderError(errorParser(response));
|
||
|
}
|
||
|
}, this);
|
||
|
this.add_related_model(this._queryGeometryModel);
|
||
|
this.add_related_model(this._querySchemaModel);
|
||
|
this.add_related_model(this._rowsCollection);
|
||
|
},
|
||
|
|
||
|
_renderQueryState: function () {
|
||
|
var querySchemaStatus = this._querySchemaModel.get('status');
|
||
|
var nodeReady = this._querySchemaModel.get('ready');
|
||
|
var geometryStatus = this._queryGeometryModel.get('status');
|
||
|
var rowsCollectionStatus = this._rowsCollection.getStatusValue();
|
||
|
|
||
|
if (nodeReady) {
|
||
|
if (querySchemaStatus === 'unavailable' && geometryStatus === 'unavailable' ||
|
||
|
rowsCollectionStatus === 'unavailable') {
|
||
|
this._renderError(this._querySchemaModel.get('query_errors'));
|
||
|
} else {
|
||
|
this._renderLoading();
|
||
|
}
|
||
|
} else {
|
||
|
this._renderLoading();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_renderLoading: function () {
|
||
|
this.$el.html(
|
||
|
renderLoading({
|
||
|
title: _t('components.table.rows.loading.title')
|
||
|
})
|
||
|
);
|
||
|
},
|
||
|
|
||
|
_renderError: function (desc) {
|
||
|
var view = new ErrorView({
|
||
|
title: _t('components.table.rows.error.title'),
|
||
|
desc: desc || _t('components.table.rows.error.desc')
|
||
|
});
|
||
|
this.addView(view);
|
||
|
this.$el.html(view.render().el);
|
||
|
},
|
||
|
|
||
|
_renderNoRows: function () {
|
||
|
this.$el.html(
|
||
|
tableNoRowsTemplate({
|
||
|
page: this._tableViewModel.get('page'),
|
||
|
customQuery: this._tableViewModel.isCustomQueryApplied()
|
||
|
})
|
||
|
);
|
||
|
},
|
||
|
|
||
|
_initPaginator: function () {
|
||
|
var paginatorView = new TablePaginatorView({
|
||
|
rowsCollection: this._rowsCollection,
|
||
|
tableViewModel: this._tableViewModel,
|
||
|
scrollToBottom: this._scrollToBottom.bind(this)
|
||
|
});
|
||
|
|
||
|
// Bug in Chrome with position:fixed :(, so we have to choose body as
|
||
|
// parent
|
||
|
var $el = $('body');
|
||
|
// But if we have chosen relativePositionated, we should add close
|
||
|
// to the table view
|
||
|
if (this._tableViewModel.get('relativePositionated')) {
|
||
|
$el = this.$el.closest('.Table').parent();
|
||
|
}
|
||
|
|
||
|
$el.append(paginatorView.render().el);
|
||
|
this.addView(paginatorView);
|
||
|
},
|
||
|
|
||
|
_initScrollBinding: function () {
|
||
|
$('.Table').scroll(this._hideContextMenu);
|
||
|
this.$('.js-tbody').scroll(this._hideContextMenu);
|
||
|
},
|
||
|
|
||
|
_destroyScrollBinding: function () {
|
||
|
$('.Table').off('scroll', this._hideContextMenu);
|
||
|
this.$('.js-tbody').off('scroll', this._hideContextMenu);
|
||
|
},
|
||
|
|
||
|
_renderBodyRow: function (mdl) {
|
||
|
var view = new TableBodyRowView({
|
||
|
model: mdl,
|
||
|
columnsCollection: this._columnsCollection,
|
||
|
simpleGeometry: this._queryGeometryModel.get('simple_geom'),
|
||
|
canHideColumns: this._canHideColumns,
|
||
|
tableViewModel: this._tableViewModel
|
||
|
});
|
||
|
this.addView(view);
|
||
|
this.$('.js-tbody').append(view.render().el);
|
||
|
},
|
||
|
|
||
|
_onRemoveRow: function () {
|
||
|
if (!this._rowsCollection.size()) {
|
||
|
this._rowsCollection.resetFetch();
|
||
|
this._queryGeometryModel.resetFetch();
|
||
|
|
||
|
var page = this._tableViewModel.get('page');
|
||
|
if (page > 0) {
|
||
|
this._tableViewModel.set('page', page - 1);
|
||
|
} else {
|
||
|
this.render();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_hasContextMenu: function () {
|
||
|
return this._menuView;
|
||
|
},
|
||
|
|
||
|
_hideContextMenu: function () {
|
||
|
this._unhighlightCell();
|
||
|
this._destroyScrollBinding();
|
||
|
this._menuView.collection.unbind(null, null, this);
|
||
|
this.removeView(this._menuView);
|
||
|
this._menuView.clean();
|
||
|
delete this._menuView;
|
||
|
},
|
||
|
|
||
|
_highlightCell: function ($tableCellItem, $tableRow) {
|
||
|
$tableCellItem.addClass('is-highlighted');
|
||
|
$tableRow.addClass('is-highlighted');
|
||
|
},
|
||
|
|
||
|
_unhighlightCell: function () {
|
||
|
this.$('.Table-cellItem.is-highlighted, .Table-row.is-highlighted').removeClass('is-highlighted');
|
||
|
},
|
||
|
|
||
|
_showContextMenu: function (ev) {
|
||
|
var self = this;
|
||
|
var position = { x: ev.clientX, y: ev.clientY };
|
||
|
var $tableRow = $(ev.target).closest('.Table-row');
|
||
|
var $tableCellItem = $(ev.target).closest('.Table-cellItem');
|
||
|
var modelCID = $tableRow.data('model');
|
||
|
var attribute = $tableCellItem.data('attribute');
|
||
|
var rowModel = self._rowsCollection.get({ cid: modelCID });
|
||
|
var menuItems = [];
|
||
|
|
||
|
menuItems.push({
|
||
|
label: _t('components.table.rows.options.copy'),
|
||
|
val: 'copy',
|
||
|
action: function () {
|
||
|
self._copyValue($tableCellItem);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (!this._tableViewModel.isDisabled()) {
|
||
|
menuItems = [
|
||
|
{
|
||
|
label: _t('components.table.rows.options.edit'),
|
||
|
val: 'edit',
|
||
|
action: function () {
|
||
|
self._editCell(rowModel, attribute);
|
||
|
}
|
||
|
}, {
|
||
|
label: _t('components.table.rows.options.create'),
|
||
|
val: 'create',
|
||
|
action: function () {
|
||
|
self._addRow();
|
||
|
}
|
||
|
}
|
||
|
].concat(menuItems);
|
||
|
|
||
|
menuItems.push({
|
||
|
label: _t('components.table.rows.options.delete'),
|
||
|
val: 'delete',
|
||
|
destructive: true,
|
||
|
action: function () {
|
||
|
self._removeRow(rowModel);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// No options?, don't open anything
|
||
|
if (!menuItems.length) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var collection = new CustomListCollection(menuItems);
|
||
|
|
||
|
this._menuView = new ContextMenuView({
|
||
|
className: 'Table-rowMenu ' + ContextMenuView.prototype.className,
|
||
|
collection: collection,
|
||
|
triggerElementID: modelCID,
|
||
|
position: position
|
||
|
});
|
||
|
|
||
|
this._menuView.$el.css(
|
||
|
magicPositioner({
|
||
|
parentView: $('body'),
|
||
|
posX: position.x,
|
||
|
posY: position.y
|
||
|
})
|
||
|
);
|
||
|
|
||
|
collection.bind('change:selected', function (menuItem) {
|
||
|
var action = menuItem.get('action');
|
||
|
action && action();
|
||
|
}, this);
|
||
|
|
||
|
this._menuView.model.bind('change:visible', function (model, isContextMenuVisible) {
|
||
|
if (this._hasContextMenu() && !isContextMenuVisible) {
|
||
|
this._hideContextMenu();
|
||
|
}
|
||
|
}, this);
|
||
|
|
||
|
this._menuView.show();
|
||
|
this.addView(this._menuView);
|
||
|
|
||
|
this._highlightCell($tableCellItem, $tableRow);
|
||
|
this._initScrollBinding();
|
||
|
},
|
||
|
|
||
|
_onClick: function (ev) {
|
||
|
var isCellOptions = $(ev.target).hasClass('js-cellOptions');
|
||
|
if (isCellOptions) {
|
||
|
if (this._hasContextMenu()) {
|
||
|
this._hideContextMenu();
|
||
|
} else {
|
||
|
this._showContextMenu(ev);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_onDblClick: function (ev) {
|
||
|
var $tableCellItem = $(ev.target).closest('.Table-cellItem');
|
||
|
var isCellOptions = $(ev.target).hasClass('js-cellOptions');
|
||
|
|
||
|
if ($tableCellItem && !isCellOptions) {
|
||
|
var $tableRow = $tableCellItem.closest('.Table-row');
|
||
|
var modelCID = $tableRow.data('model');
|
||
|
var attribute = $tableCellItem.data('attribute');
|
||
|
var rowModel = this._rowsCollection.get({ cid: modelCID });
|
||
|
|
||
|
if (!this._tableViewModel.isDisabled() && rowModel && attribute && attribute !== 'cartodb_id') {
|
||
|
this._editCell(rowModel, attribute);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_copyValue: function ($el) {
|
||
|
// Work-around for Clipboard \o/
|
||
|
this._clipboard = new Clipboard($el.get(0));
|
||
|
$el.click();
|
||
|
this._clipboard.destroy();
|
||
|
},
|
||
|
|
||
|
_addRow: function () {
|
||
|
addTableRowOperation({
|
||
|
rowsCollection: this._rowsCollection
|
||
|
});
|
||
|
},
|
||
|
|
||
|
_initEditorScrollBinding: function () {
|
||
|
$('.Table').scroll(this._closeEditor);
|
||
|
this.$('.js-tbody').scroll(this._closeEditor);
|
||
|
},
|
||
|
|
||
|
_destroyEditorScrollBinding: function () {
|
||
|
$('.Table').unbind('scroll', this._closeEditor);
|
||
|
this.$('.js-tbody').unbind('scroll', this._closeEditor);
|
||
|
},
|
||
|
|
||
|
_closeEditor: function () {
|
||
|
this._unhighlightCell();
|
||
|
this._destroyEditorScrollBinding();
|
||
|
this._editors.unbind(null, null, this);
|
||
|
this._editors.destroy();
|
||
|
},
|
||
|
|
||
|
_saveValue: function (rowModel, attribute, newValue) {
|
||
|
if (rowModel.get(attribute) !== newValue) {
|
||
|
editCellOperation({
|
||
|
rowModel: rowModel,
|
||
|
attribute: attribute,
|
||
|
newValue: newValue
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_doCellEdition: function (rowModel, attribute) {
|
||
|
var $tableRow = this.$('[data-model="' + rowModel.cid + '"]');
|
||
|
var $tableCell = $tableRow.find('[data-attribute="' + attribute + '"]');
|
||
|
var $options = $tableCell.find('.js-cellOptions');
|
||
|
var columnModel = _.first(this._columnsCollection.where({ name: attribute }));
|
||
|
var type = columnModel.get('type');
|
||
|
|
||
|
this._highlightCell($tableCell, $tableRow);
|
||
|
this._initEditorScrollBinding();
|
||
|
|
||
|
var model = new EditorsModel({
|
||
|
type: type,
|
||
|
value: rowModel.get(attribute)
|
||
|
});
|
||
|
|
||
|
this._editors.bind('destroyedEditor', this._closeEditor, this);
|
||
|
this._editors.bind('confirmedEditor', function () {
|
||
|
if (model.isValid()) {
|
||
|
this._saveValue(
|
||
|
rowModel,
|
||
|
attribute,
|
||
|
model.get('value')
|
||
|
);
|
||
|
}
|
||
|
}, this);
|
||
|
|
||
|
var position = $options.offset();
|
||
|
|
||
|
if ($tableCell.index() > 1) {
|
||
|
position.right = window.innerWidth - position.left;
|
||
|
delete position.left;
|
||
|
}
|
||
|
|
||
|
if (this._rowsCollection.size() > 4 && ($tableRow.index() + 2) >= (this._rowsCollection.size() - 1)) {
|
||
|
position.bottom = window.innerHeight - position.top;
|
||
|
delete position.top;
|
||
|
} else {
|
||
|
position.top = position.top + 20;
|
||
|
}
|
||
|
|
||
|
var View = EDITORS_MAP[type];
|
||
|
|
||
|
if (!View) {
|
||
|
View = EDITORS_MAP['default'];
|
||
|
}
|
||
|
|
||
|
this._editors.create(
|
||
|
function (editorModel) {
|
||
|
return new View({
|
||
|
editorModel: editorModel,
|
||
|
model: model
|
||
|
});
|
||
|
},
|
||
|
position
|
||
|
);
|
||
|
},
|
||
|
|
||
|
_editCell: function (rowModel, attribute) {
|
||
|
var callback = this._doCellEdition.bind(this, rowModel, attribute);
|
||
|
rowModel.fetchRowIfGeomIsNotLoaded(callback);
|
||
|
},
|
||
|
|
||
|
_removeRow: function (rowModel) {
|
||
|
var self = this;
|
||
|
|
||
|
this._modals.create(
|
||
|
function (modalModel) {
|
||
|
return new ConfirmationModalView({
|
||
|
modalModel: modalModel,
|
||
|
template: require('./modals-templates/remove-table-row.tpl'),
|
||
|
renderOpts: {
|
||
|
cartodb_id: rowModel.get('cartodb_id')
|
||
|
},
|
||
|
loadingTitle: _t('components.table.rows.destroy.loading', {
|
||
|
cartodb_id: rowModel.get('cartodb_id')
|
||
|
}),
|
||
|
runAction: function () {
|
||
|
removeTableRowOperation({
|
||
|
tableViewModel: self._tableViewModel,
|
||
|
rowModel: rowModel,
|
||
|
onSuccess: function () {
|
||
|
modalModel.destroy();
|
||
|
},
|
||
|
onError: function (e) {
|
||
|
modalModel.destroy();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
);
|
||
|
},
|
||
|
|
||
|
_scrollToBottom: function () {
|
||
|
var tbodyHeight = this.$('.js-tbody').get(0).scrollHeight;
|
||
|
this.$('.js-tbody').animate({
|
||
|
scrollTop: tbodyHeight
|
||
|
}, 'slow');
|
||
|
},
|
||
|
|
||
|
clean: function () {
|
||
|
this._destroyScrollBinding();
|
||
|
CoreView.prototype.clean.apply(this);
|
||
|
}
|
||
|
|
||
|
});
|