CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/builder/components/table/head/table-head-item-view.js

Summary

Maintainability
D
2 days
Test Coverage
var $ = require('jquery');
var CoreView = require('backbone/core-view');
var template = require('./table-head-item.tpl');
var ContextMenuView = require('builder/components/context-menu/context-menu-view');
var CustomListCollection = require('builder/components/custom-list/custom-list-collection');
var TableHeadOptionsItemView = require('./table-head-options-item-view');
var tableHeadOptionsItemTemplate = require('./table-head-options-item.tpl');
var ConfirmationModalView = require('builder/components/modals/confirmation/modal-confirmation-view');
var QueryColumnModel = require('builder/data/query-column-model');
var addColumnOperation = require('builder/components/table/operations/table-add-column');
var renameColumnOperation = require('builder/components/table/operations/table-rename-column');
var changeColumnTypeOperation = require('builder/components/table/operations/table-change-column-type');
var removeTableColumnOperation = require('builder/components/table/operations/table-remove-column');

var ENTER_KEY_CODE = 13;
var ESC_KEY_CODE = 27;

/*
 *  Main table view
 */

module.exports = CoreView.extend({

  className: 'Table-headItem',
  tagName: 'th',

  events: {
    'dblclick .js-attribute': '_onAttributeDblClicked',
    'focusout .js-attribute': '_saveNewName',
    'click .js-columnOptions': '_showContextMenu'
  },

  initialize: function (opts) {
    if (!opts.tableViewModel) throw new Error('tableViewModel is required');
    if (!opts.columnsCollection) throw new Error('columnsCollection is required');
    if (!opts.modals) throw new Error('modals is required');

    this._tableViewModel = opts.tableViewModel;
    this._columnsCollection = opts.columnsCollection;
    this._modals = opts.modals;

    this._hideContextMenu = this._hideContextMenu.bind(this);
    this._onKeyDown = this._onKeyDown.bind(this);
  },

  render: function () {
    this.clearSubViews();
    var columnName = this.model.get('name');
    this.$el.html(
      template({
        name: columnName,
        type: this.model.get('type'),
        isOrderBy: this._tableViewModel.get('order_by') === columnName,
        sortBy: this._tableViewModel.get('sort_order'),
        geometry: this.options.simpleGeometry
      })
    );
    return this;
  },

  _hasContextMenu: function () {
    return this._menuView;
  },

  _hideContextMenu: function () {
    this._unhighlightHead();
    this._destroyScrollBinding();
    this._menuView.collection.unbind(null, null, this);
    this.removeView(this._menuView);
    this._menuView.clean();
    delete this._menuView;
  },

  _highlightHead: function () {
    this.$el.addClass('is-highlighted');
  },

  _unhighlightHead: function () {
    this.$el.removeClass('is-highlighted');
  },

  _initScrollBinding: function () {
    $('.Table').scroll(this._hideContextMenu);
  },

  _destroyScrollBinding: function () {
    $('.Table').off('scroll', this._hideContextMenu);
  },

  _showContextMenu: function (ev) {
    var self = this;
    var position = { x: ev.clientX, y: ev.clientY };
    var elementIndex = this.$el.index();
    var modelCID = this.model.cid;
    var columnName = this.model.get('name');

    this._highlightHead();

    var menuItems = [{
      label: _t('components.table.columns.options.order'),
      val: 'order',
      isOrderBy: this._tableViewModel.get('order_by') === columnName,
      sortBy: this._tableViewModel.get('sort_order'),
      action: function (model) {
        self._tableViewModel.set({
          sort_order: model.get('sort'),
          order_by: columnName
        });
      }
    }];

    if (!this._tableViewModel.isDisabled()) {
      if (!this.model.isCartoDBIDColumn() && !this.model.isGeometryColumn()) {
        menuItems = menuItems.concat([
          {
            label: _t('components.table.columns.options.rename'),
            val: 'rename',
            action: function () {
              self._startEditing();
            }
          }, {
            label: _t('components.table.columns.options.change'),
            type: this.model.get('type'),
            isLastColumns: (this._columnsCollection.size() - elementIndex) < 3,
            val: 'change',
            action: function (model) {
              self._changeColumnType(model.get('type'));
            }
          }
        ]);
      }

      menuItems.push({
        label: _t('components.table.columns.options.create'),
        val: 'create',
        action: function () {
          self._addColumn();
        }
      });

      if (!this.model.isCartoDBIDColumn() && !this.model.isGeometryColumn()) {
        menuItems.push(
          {
            label: _t('components.table.columns.options.delete'),
            val: 'delete',
            destructive: true,
            action: function () {
              self._removeColumn();
            }
          }
        );
      }
    }

    var collection = new CustomListCollection(menuItems);

    this._menuView = new ContextMenuView({
      className: ContextMenuView.prototype.className + ' Table-columnMenu',
      collection: collection,
      itemTemplate: tableHeadOptionsItemTemplate,
      itemView: TableHeadOptionsItemView,
      triggerElementID: modelCID,
      position: position
    });

    collection.bind('change:selected', function (menuItem) {
      var action = menuItem.get('action');
      action && action(menuItem);
    }, 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._initScrollBinding();
  },

  _addColumn: function () {
    addColumnOperation({
      columnsCollection: this._columnsCollection
    });
  },

  _changeColumnType: function (newType) {
    var self = this;
    var oldType = this.model.get('type');
    var isTypeChangeDestructive = QueryColumnModel.isTypeChangeDestructive(oldType, newType);
    if (!isTypeChangeDestructive) {
      changeColumnTypeOperation({
        columnModel: this.model,
        newType: newType
      });
    } else {
      this._modals.create(
        function (modalModel) {
          return new ConfirmationModalView({
            modalModel: modalModel,
            template: require('./modals-templates/change-table-column-type.tpl'),
            renderOpts: {
              columnName: self.model.get('name'),
              newType: newType
            },
            loadingTitle: _t('components.table.columns.change-type.loading', {
              columnName: self.model.get('name')
            }),
            runAction: function () {
              changeColumnTypeOperation({
                columnModel: self.model,
                newType: newType,
                onSuccess: function () {
                  modalModel.destroy();
                },
                onError: function (e) {
                  modalModel.destroy();
                }
              });
            }
          });
        }
      );
    }
  },

  _removeColumn: function () {
    var self = this;
    this._modals.create(
      function (modalModel) {
        return new ConfirmationModalView({
          modalModel: modalModel,
          template: require('./modals-templates/remove-table-column.tpl'),
          renderOpts: {
            name: self.model.get('name')
          },
          loadingTitle: _t('components.table.columns.destroy.loading', {
            columnName: self.model.get('name')
          }),
          runAction: function () {
            removeTableColumnOperation({
              columnModel: self.model,
              onSuccess: function () {
                modalModel.destroy();
              },
              onError: function (e) {
                modalModel.destroy();
              }
            });
          }
        });
      }
    );
  },

  _onAttributeDblClicked: function () {
    if (this.model.isEditable() && !this._tableViewModel.isDisabled()) {
      this._startEditing();
    }
  },

  _initRenameBinds: function () {
    $(document).bind('keydown', this._onKeyDown);
  },

  _destroyRenameBinds: function () {
    $(document).unbind('keydown', this._onKeyDown);
  },

  _onKeyDown: function (ev) {
    var keyCode = ev.which;
    if (keyCode === ENTER_KEY_CODE) {
      this._saveNewName();
    } else if (keyCode === ESC_KEY_CODE) {
      this._finishEditing();
    }
  },

  _startEditing: function () {
    this.$('.js-attribute')
      .addClass('is-active')
      .removeAttr('readonly');

    this._initRenameBinds();
  },

  _finishEditing: function () {
    this.$('.js-attribute')
      .val(this.model.get('name'))
      .removeClass('is-active')
      .attr('readonly', '');

    this._destroyRenameBinds();
  },

  _saveNewName: function () {
    if (this.model.isEditable()) {
      var newName = this.$('.js-attribute').val();
      var columnModel = this.model;
      var oldName = columnModel.get('name');

      if (oldName !== newName && newName !== '') {
        this._modals.create(
          function (modalModel) {
            return new ConfirmationModalView({
              modalModel: modalModel,
              template: require('./modals-templates/rename-table-column.tpl'),
              renderOpts: {
                columnName: oldName,
                newName: newName
              },
              loadingTitle: _t('components.table.columns.rename.loading', {
                columnName: oldName,
                newName: newName
              }),
              runAction: function () {
                renameColumnOperation({
                  columnModel: columnModel,
                  newName: newName,
                  onSuccess: function () {
                    modalModel.destroy();
                  },
                  onError: function (e) {
                    modalModel.destroy();
                  }
                });
              }
            });
          }
        );
      }
    }

    // Setting old name just in case user cancel it
    this._finishEditing();
  },

  focusInput: function () {
    this._startEditing();
    this.$('.js-attribute').select();
  },

  clean: function () {
    this._destroyRenameBinds();
    this._destroyScrollBinding();
    CoreView.prototype.clean.apply(this);
  }
});