cartodb-4.42/lib/assets/javascripts/dashboard/views/public-dataset/table-view/table-view.js
2024-04-06 05:25:13 +00:00

540 lines
17 KiB
JavaScript

const $ = require('jquery');
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const Table = require('dashboard/components/table/table');
const templateSQLViewNotice = require('./sql-view-notice.tpl');
const templateSQLLoading = require('./sql-loading.tpl');
const templateEmptySQL = require('./empty-sql.tpl');
const templateTablePaginationLoaders = require('./table-pagination-loaders.tpl');
const RowView = require('./row-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel'
];
module.exports = Table.extend({
classLabel: 'cdb.admin.TableView',
events: CoreView.extendEvents({
'click .clearview': '_clearView',
'click .sqlview .export_query': '_tableFromQuery',
'click .noRows': 'addEmptyRow'
}),
rowView: RowView,
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
Table.prototype.initialize.call(this);
this.options.row_header = true;
this.globalError = this.options.globalError;
this.vis = this.options.vis;
this.user = this.options.user;
this._editorsOpened = null;
this.initializeBindings();
this.initPaginationAndScroll();
},
/**
* Append all the bindings needed for this view
* @return undefined
*/
initializeBindings: function () {
_.bindAll(this, 'render', 'rowSaving', 'addEmptyRow',
'_checkEmptyTable', '_forceScroll', '_scrollMagic',
'rowChanged', 'rowSynched', '_startPagination', '_finishPagination',
'rowFailed', 'rowDestroyed', 'emptyTable');
this.model.data().bind('newPage', this.newPage, this);
// this.model.data().bind('loadingRows', this._startPagination);
this.model.data().bind('endLoadingRows', this._finishPagination);
this.bind('cellDblClick', this._editCell, this);
this.bind('createRow', () => {
this._checkEmptyTable();
});
this.model.bind('change:dataSource', this._onSQLView, this);
// when model changes the header is re rendered so the notice should be added
// this.model.bind('change', this._onSQLView, this);
this.model.bind('dataLoaded', () => {
// this._checkEmptyTable();
this._forceScroll();
}, this);
this.model.bind('change:permission', this._checkEmptyTable, this);
this.model.bind('change:isSync', this._swicthEnabled, this);
this._swicthEnabled();
// Geocoder binding
this.options.geocoder.bind('geocodingComplete geocodingError geocodingCanceled', function () {
this.notice(_t('loaded'));
}, this);
this.add_related_model(this.options.geocoder);
},
initPaginationAndScroll: function () {
var topReached = false;
var bottomReached = false;
// Initialize moving header and loaders when scrolls
this.scroll_position = { x: $(window).scrollLeft(), y: $(window).scrollTop(), last: 'vertical' };
$(window).scroll(this._scrollMagic);
// Pagination
var SCROLL_BACK_PIXELS = 2;
this.checkScrollTimer = setInterval(() => {
if (!this.$el.is(':visible') || this.model.data().isFetchingPage()) {
return;
}
var pos = $(this).scrollTop();
var d = this.model.data();
// do not let to fetch previous pages
// until the user dont scroll back a little bit
// see comments below
if (pos > SCROLL_BACK_PIXELS) {
topReached = false;
}
var pageSize = $(window).height() - this.$el.offset().top;
var tableHeight = this.$('tbody').height();
var realPos = pos + pageSize;
if (tableHeight < pageSize) {
return;
}
// do not let to fetch previous pages
// until the user dont scroll back a little bit
// if we dont do this when the user reach the end of the page
// and there are more rows than max_rows, the rows form the beggining
// are removed and the scroll keeps at the bottom so a new page is loaded
// doing this the user have to move the scroll a little bit (2 px)
// in order to load the page again
if (realPos < tableHeight - SCROLL_BACK_PIXELS) {
bottomReached = false;
}
if (realPos >= tableHeight) {
if (!bottomReached) {
// Simulating loadingRows event
if (!d.lastPage) this._startPagination('down');
setTimeout(function () {
d.loadPageAtBottom();
}, 600);
}
bottomReached = true;
} else if (pos <= 0) {
if (!topReached) {
// Simulating loadingRows event
if (d.pages && d.pages[0] != 0) this._startPagination('up'); // eslint-disable-line eqeqeq
setTimeout(function () {
d.loadPageAtTop();
}, 600);
}
topReached = true;
}
this._setUpPagination(d);
}, 300);
this.bind('clean', function () {
clearInterval(this.checkScrollTimer);
}, this);
},
needsRender: function (table) {
if (!table) return true;
var ca = table.changedAttributes();
if (ca.geometry_types && _.keys(ca).length === 1) {
return false;
}
return true;
},
render: function (args) {
if (!this.needsRender(args)) return;
Table.prototype.render.call(this, args);
if (this.model.isInSQLView()) {
this._onSQLView();
}
this._swicthEnabled();
this.trigger('render');
},
_renderHeader: function () {
var thead = Table.prototype._renderHeader.apply(this);
// New custom shadow (better performance)
thead.append($('<div>').addClass('shadow'));
return thead;
},
addColumn: function (column) {
this.newColumnName = 'column_' + new Date().getTime();
this.model.addColumn(this.newColumnName, 'string');
this.unbind('render', this._highlightColumn, this);
this.bind('render', this._highlightColumn, this);
},
_highlightColumn: function () {
if (this.newColumnName) {
var $th = this.$("a[href='#" + this.newColumnName + "']").parents('th');
var position = $th.index();
if (position) {
setTimeout(function () {
var windowWidth = $(window).width();
if ($th && $th.position()) {
var centerPosition = $th.position().left - windowWidth / 2 + $th.width() / 2;
$(window).scrollLeft(centerPosition);
}
this.$("[data-x='" + position + "']").addClass('is-highlighted');
}, 300);
this.unbind('render', this._highlightColumn, this);
}
}
},
/**
* Take care if the table needs space at top and bottom
* to show the loaders.
*/
_setUpPagination: function (d) {
var pages = d.pages;
// Check if the table is not in the first page
if (pages.length > 0 && pages[0] > 0) {
// In that case, add the paginator-up loader and leave it ready
// when it is necessary
if (this.$el.find('tfoot.page_loader.up').length == 0) { // eslint-disable-line eqeqeq
this.$el.append(templateTablePaginationLoaders({ direction: 'up' }));
}
// Table now needs some space at the top to show the loader
this.$el.parent().addClass('page_up');
} else {
// Loader is not needed and table doesn't need any space at the top
this.$el.parent().removeClass('page_up');
}
// Checks if we are in the last page
if (!d.lastPage) {
// If not, let's prepare the paginator-down
if (this.$el.find('tfoot.page_loader.down').length == 0) { // eslint-disable-line eqeqeq
this.$el.append(templateTablePaginationLoaders({ direction: 'down' }));
}
// Let's say to the table that we have paginator-down
this.$el.parent().addClass('page_down');
} else {
// Loader is not needed and table doesn't need any space at the bottom
this.$el.parent().removeClass('page_down');
}
},
/**
* What to do when a pagination starts
*/
_startPagination: function (updown) {
// Loader... move on buddy!
this.$el.find('.page_loader.' + updown + '').addClass('active');
},
/**
* What to do when a pagination finishes
*/
_finishPagination: function (page, updown) {
// If we are in a different page than 0, and we are paginating up
// let's move a little bit the scroll to hide the loader again
// HACKY
if (page != 0 && updown == 'up') { // eslint-disable-line eqeqeq
setTimeout(function () {
$(window).scrollTop(180);
}, 300);
}
this.$el.find('.page_loader.active').removeClass('active');
},
_onSQLView: function () {
// bind each time we change dataSource because table unbind
// all the events from sqlView object each time useSQLView is called
this.$('.sqlview').remove();
this.options.sqlView.unbind('reset error', this._renderSQLHeader, this);
this.options.sqlView.unbind('loading', this._renderLoading, this);
this.options.sqlView.bind('loading', this._renderLoading, this);
this.options.sqlView.bind('reset', this._renderSQLHeader, this);
this.options.sqlView.bind('error', this._renderSQLHeader, this);
this._renderSQLHeader();
},
_renderLoading: function (opts) {
opts = opts || {};
this.cleanEmptyTableInfo();
if (!opts.add) {
this._renderBodyTemplate(templateSQLLoading);
}
},
_renderSQLHeader: function () {
if (this.model.isInSQLView()) {
var empty = this.isEmptyTable();
this.$('thead').find('.sqlview').remove();
this.$('thead').append(
templateSQLViewNotice({
empty: empty,
isVisualization: this.vis.isVisualization(),
warnMsg: null
})
);
this.$('thead > tr').css('height', 64 + 42);
if (this.isEmptyTable()) {
this.addEmptySQLIfo();
}
this._moveInfo();
}
},
// depending if the sync is enabled add or remove a class
_swicthEnabled: function () {
// Synced?
this.$el[ this.model.isSync() ? 'addClass' : 'removeClass' ]('synced');
// Visualization?
this.$el[ this.vis.isVisualization() ? 'addClass' : 'removeClass' ]('vis');
},
_clearView: function (e) {
if (e) e.preventDefault();
this.options.layer.clearSQLView();
return false;
},
_tableFromQuery: function (e) {
throw new Error('Method not migrated, check original implementation');
},
/**
* Function to control the scroll in the table (horizontal and vertical)
*/
_scrollMagic: function (ev) {
var actual_scroll_position = { x: $(window).scrollLeft(), y: $(window).scrollTop() };
if (this.scroll_position.x != actual_scroll_position.x) { // eslint-disable-line eqeqeq
this.scroll_position.x = actual_scroll_position.x;
this.$el.find('thead').addClass('horizontal');
// If last change was vertical
if (this.scroll_position.last == 'vertical') { // eslint-disable-line eqeqeq
this.scroll_position.x = actual_scroll_position.x;
this.$el.find('thead > tr > th > div > div:not(.dropdown)')
.removeAttr('style')
.css('top', actual_scroll_position.y + 'px');
this.scroll_position.last = 'horizontal';
}
} else if (this.scroll_position.y != actual_scroll_position.y) { // eslint-disable-line eqeqeq
this.scroll_position.y = actual_scroll_position.y;
this.$el.find('thead').removeClass('horizontal');
// If last change was horizontal
if (this.scroll_position.last == 'horizontal') { // eslint-disable-line eqeqeq
this.$el.find('thead > tr > th > div > div:not(.dropdown)')
.removeAttr('style')
.css({'marginLeft': '-' + actual_scroll_position.x + 'px'});
this.scroll_position.last = 'vertical';
}
}
},
/**
* Move the info content if the panel is opened or hidden.
* - Query info if query is applied
* - Query loader if query is appliying in that moment
* - Add some padding to last column of the content to show them
*/
_moveInfo: function (type) {
if (type == 'show') { // eslint-disable-line eqeqeq
this.$el
.removeClass('narrow')
.addClass('displaced');
} else if (type == 'narrow') { // eslint-disable-line eqeqeq
this.$el.addClass('displaced narrow');
} else if (type == 'hide') { // eslint-disable-line eqeqeq
this.$el.removeClass('displaced narrow');
} else {
// Check from the beginning if the right menu is openned, isOpen from
// the menu is not working properly
if ($('.table_panel').length > 0) {
var opened = $('.table_panel').css('right').replace('px', '') == 0; // eslint-disable-line eqeqeq
if (!opened) {
this.$el.removeClass('displaced');
}
}
}
},
_getEditor: function (columnType, opts) {
throw new Error('Method not migrated, check original implementation');
},
closeEditor: function () {
if (this._editorsOpened) {
this._editorsOpened.hide();
this._editorsOpened.clean();
}
},
_editCell: function (e, cell, x, y) {
throw new Error('Method not migrated, check original implementation');
},
headerView: function (column) {
throw new Error('Method not migrated, check original implementation');
},
/**
* Checks if the table has any rows, and if not, launch the method for showing the appropiate view elements
* @method _checkEmptyTable
*/
_checkEmptyTable: function () {
if (this.isEmptyTable()) {
this.addEmptyTableInfo();
} else {
this.cleanEmptyTableInfo();
}
},
/**
* Force the table to be at the beginning
* @method _forceScroll
*/
_forceScroll: function (ev) {
$(window).scrollLeft(0);
},
_renderEmpty: function () {
this.addEmptyTableInfo();
},
/**
* Adds the view elements associated with no content in the table
* @method addemptyTableInfo
*/
addEmptyTableInfo: function () {
throw new Error('Method not migrated, check original implementation');
},
/**
* Adds the view elements associated with no content in the table when a SQL is applied
* @method addEmptySQLIfo
*/
addEmptySQLIfo: function () {
if (this.model.isInSQLView()) {
this._renderBodyTemplate(templateEmptySQL);
}
},
_renderBodyTemplate: function (template) {
this.$('tbody').html('');
this.$('tfoot').remove();
this.$el.hide();
// Check if panel is opened to move the loader some bit left
var panel_opened = false;
if ($('.table_panel').length > 0) {
panel_opened = $('.table_panel').css('right').replace('px', '') == 0; // eslint-disable-line eqeqeq
}
var content = template({ config: this._configModel, panel_opened });
var $footer = $('<tfoot class="sql_loader"><tr><td colspan="100">' + content + '</td></tr></tfoot>');
this.$el.append($footer);
this.$el.fadeIn();
},
/**
* Removes from the view the no-content elements
* @method cleanEmptyTableInfo
*/
cleanEmptyTableInfo: function () {
this.$('tfoot').fadeOut('fast', function () {
$(this).remove();
});
this.$('.noRows').slideUp('fast', function () {
$(this).remove();
});
},
notice: function (text, type, time) {
this.globalError.showError(text, type, time);
},
/**
* Add a new row and removes the empty table view elemetns
* @method addEmptyRow
* @todo: (xabel) refactor this to include a "addRow" method in _models[0]
*/
addEmptyRow: function () {
this.dataModel.addRow({ at: 0 });
this.cleanEmptyTableInfo();
},
/**
* Captures the saving event from the row and produces a notification
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
* to throw another event in the model to be captured by some view
*/
rowSaving: function () {
this.notice('Saving your edit', 'load', -1);
},
/**
* Captures the change event from the row and produces a notification
* @method rowSynched
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
* to throw another event in the model to be captured by some view
*/
rowSynched: function () {
this.notice('Sucessfully saved');
},
/**
* Captures the change event from the row and produces a notification
* @method rowSynched
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
* to throw another event in the model to be captured by some view
*/
rowFailed: function () {
this.notice('Oops, there has been an error saving your changes.', 'error');
},
/**
* Captures the destroy event from the row and produces a notification
* @method rowDestroyed
*/
rowDestroying: function () {
this.notice('Deleting row', 'load', -1);
},
/**
* Captures the sync after a destroy event from the row and produces a notification
* @method rowDestroyed
*/
rowDestroyed: function () {
this.notice('Sucessfully deleted');
this._checkEmptyTable();
}
});