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($('
').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 = $('' + content + ''); 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(); } });