(function() { /** * table view shown in admin */ cdb.admin.TableView = cdb.ui.common.Table.extend({ classLabel: 'cdb.admin.TableView', events: cdb.core.View.extendEvents({ 'click .clearview': '_clearView', 'click .sqlview .export_query': '_tableFromQuery', 'click .noRows': 'addEmptyRow' }), rowView: cdb.admin.RowView, initialize: function() { var self = this; this.elder('initialize'); 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() { var self = this; _.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', function() { self._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', function() { //self._checkEmptyTable(); self._forceScroll(); }, this); this.model.bind('change:permission', this._checkEmptyTable, this); this.model.bind('change:isSync', this._swicthEnabled, this); this._swicthEnabled(); // Actions triggered in the right panel cdb.god.bind("panel_action", function(action) { self._moveInfo(action); }, this); this.add_related_model(cdb.god); // 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 self = this; 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(function() { if(!self.$el.is(":visible") || self.model.data().isFetchingPage()) { return; } var pos = $(this).scrollTop(); var d = self.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() - self.$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) self._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) self._startPagination('up'); setTimeout(function() { d.loadPageAtTop() },600); } topReached = true; } self._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; this.elder('render', args); if (this.model.isInSQLView()) { this._onSQLView(); } this._swicthEnabled(); this.trigger('render'); }, _renderHeader: function() { var thead = cdb.ui.common.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) { this.$el.append(this.getTemplate('table/views/table_pagination_loaders')({ 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) { this.$el.append(this.getTemplate('table/views/table_pagination_loaders')({ 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") { 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('table/views/sql_loading'); } }, _renderSQLHeader: function() { var self = this; if(self.model.isInSQLView()) { var empty = self.isEmptyTable(); self.$('thead').find('.sqlview').remove(); self.$('thead').append( self.getTemplate('table/views/sql_view_notice')({ empty: empty, isVisualization: self.vis.isVisualization(), warnMsg: null }) ); self.$('thead > tr').css('height', 64 + 42); if(self.isEmptyTable()) { self.addEmptySQLIfo(); } self._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) { e.preventDefault(); var duplicate_dialog = new cdb.editor.DuplicateDatasetView({ model: this.model, user: this.user, clean_on_hide: true }); duplicate_dialog.appendToBody(); }, /** * 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) { 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") { 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) { 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") { 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") { this.$el .removeClass('narrow') .addClass('displaced'); } else if (type == "narrow") { this.$el.addClass('displaced narrow') } else if (type == "hide") { 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 ? true : false; if (!opened) { this.$el.removeClass('displaced'); } } } }, _getEditor: function(columnType, opts) { var editors = { 'string': cdb.admin.StringField, 'number': cdb.admin.NumberField, 'date': cdb.admin.DateField, 'geometry': cdb.admin.GeometryField, 'timestamp with time zone': cdb.admin.DateField, 'timestamp without time zone': cdb.admin.DateField, 'boolean': cdb.admin.BooleanField }; var editorExists = _.filter(editors, function(a,i) { return i === columnType }).length > 0; if(columnType !== "undefined" && editorExists) { return editors[columnType]; } else { return editors['string'] } }, closeEditor: function() { if (this._editorsOpened) { this._editorsOpened.hide(); this._editorsOpened.clean(); } }, _editCell: function(e, cell, x, y) { var self = this; // Clean and close previous cell editor this.closeEditor(); var column = self.model.columnName(x-1); var columnType = this.model.getColumnType(column); if (this.model.isReservedColumn(column) && !this.model.isReadOnly() && columnType!='geometry') { return; } var row = self.model.data().getRowAt(y); var initial_value = ''; if(self.model.data().getCell(y, column) === 0 || self.model.data().getCell(y, column) === '0') { initial_value = '0'; } else if (self.model.data().getCell(y, column) !== undefined) { initial_value = self.model.data().getCell(y, column); } // dont let generic editor if(column == 'the_geom') { columnType = 'geometry' } var prevRow = _.clone(row.toJSON()); var dlg = this._editorsOpened = new cdb.admin.SmallEditorDialog({ value: initial_value, column: column, row: row, rowNumber: y, readOnly: this.model.isReadOnly(), editorField: this._getEditor(columnType), res: function(new_value) { if(!_.isEqual(new_value, prevRow[column])) { // do not use save error callback since it avoid model error method to be called row.bind('error', function editError() { row.unbind('error', editError); // restore previopis on error row.set(column, prevRow[column]); }); row .save(column, new_value) .done(function(a){ self.model.trigger('data:saved'); }); } } }); if(!dlg) { cdb.log.error("editor not defined for column type " + columnType); return; } // auto add to table view // Check first if the row is the first or the cell is the last :) var $td = $(e.target).closest("td") , offset = $td.offset() , $tr = $(e.target).closest("tr") , width = Math.min($td.outerWidth(), 278); // Remove header spacing from top offset offset.top = offset.top - this.$el.offset().top; if ($td.parent().index() == 0) { offset.top += 5; } else { offset.top -= 11; } if ($td.index() == ($tr.find("td").size() - 1) && $tr.find("td").size() < 2) { offset.left -= width/2; } else { offset.left -= 11; } dlg.showAt(offset.left, offset.top, width, true); }, headerView: function(column) { var self = this; if(column[1] !== 'header') { var v = new cdb.admin.HeaderView({ column: column, table: this.model, sqlView: this.options.sqlView, user: this.user, vis: this.vis }) .bind('clearView', this._clearView, this) .bind('georeference', function(column) { var dlg; var bkgPollingModel = this.options.backgroundPollingModel; var tableIsReadOnly = this.model.isSync(); var canAddGeocoding = bkgPollingModel !== "" ? bkgPollingModel.canAddGeocoding() : true; // With new modals if (!this.options.geocoder.isGeocoding() && !tableIsReadOnly && canAddGeocoding) { var dlg = new cdb.editor.GeoreferenceView({ table: this.model, user: this.user, tabs: ['lonlat', 'city', 'admin', 'postal', 'ip', 'address'], option: 'lonlat', data: { longitude: column } }); } else if (this.options.geocoder.isGeocoding() || ( !canAddGeocoding && !tableIsReadOnly )) { dlg = cdb.editor.ViewFactory.createDialogByTemplate('common/background_polling/views/geocodings/geocoding_in_progress'); } else { // If table can't geocode == is synched, return! return; } dlg.appendToBody(); }, this) .bind('applyFilter', function(column) { self.options.menu.show('filters_mod'); self.options.layer.trigger('applyFilter',column); }, this) this.addView(v); if (this.newColumnName == column[0]) { setTimeout(function() { v.renameColumn(); self.newColumnName = null; }, 300); } return v.render().el; } else { return '
'; } }, /** * 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() { if(this.$('.noRows').length == 0 && !this.model.isInSQLView() && this.model.get('schema')) { this.elder('addEmptyTableInfo'); this.$el.hide(); // Fake empty row if the table is not readonly if (!this.model.isReadOnly()) { //TODO: use row view instead of custom HTML var columnsNumber = this.model.get('schema').length; var columns = '+'; for(var i = 0; i < columnsNumber; i++) { columns += ''; } columns += ''; var columnsFooter = ''; for(var i = 0; i < columnsNumber; i++) { columnsFooter += ''; } columnsFooter += ''; var $columns = $(columns+columnsFooter) this.$el.append($columns); } this.template_base = cdb.templates.getTemplate( this.model.isReadOnly() ? 'table/views/empty_readtable_info' : 'table/views/empty_table'); var content = this.template_base(); var $footer = $('' + content + ''); this.$el.append($footer); this.$el.fadeIn(); } }, /** * 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('table/views/empty_sql'); } }, _renderBodyTemplate: function(tmpl) { 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 ? true : false; } var content = cdb.templates.getTemplate(tmpl)({ panel_opened: panel_opened }) , $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(); } }); /** * table tab controller */ cdb.admin.TableTab = cdb.core.View.extend({ className: 'table', initialize: function() { this.user = this.options.user; this.sqlView = this.options.sqlView; this.geocoder = this.options.geocoder; this.backgroundPollingModel = this.options.backgroundPollingModel; this._initBinds(); }, setActiveLayer: function(layerView) { var recreate = !!this.tableView; this.deactivated(); this.model = layerView.table; this.layer = layerView.model; this.sqlView = layerView.sqlView; if(recreate) { this.activated(); } }, _initBinds: function() { // Geocoder binding this.geocoder.bind('geocodingComplete geocodingError geocodingCanceled', function() { if (this.model.data) { this.model.data().refresh() } }, this); this.add_related_model(this.geocoder); }, _createTable: function() { this.tableView = new cdb.admin.TableView({ dataModel: this.model.data(), model: this.model, sqlView: this.sqlView, layer: this.layer, geocoder: this.options.geocoder, backgroundPollingModel: this.backgroundPollingModel, vis: this.options.vis, menu: this.options.menu, user: this.user, globalError: this.options.globalError }); }, activated: function() { if(!this.tableView) { this._createTable(); this.tableView.render(); this.render(); } }, deactivated: function() { if(this.tableView) { this.tableView.clean(); this.tableView = null; this.hasRenderedTableView = false; } }, render: function() { // Since render should be idempotent (i.e. should not append the tableView twice when called multiple times) if(this.tableView && !this.hasRenderedTableView) { this.hasRenderedTableView = true; this.$el.append(this.tableView.el); } return this; } }); })();