CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/builder/editor/layers/layer-header-view.js

Summary

Maintainability
F
4 days
Test Coverage
var CoreView = require('backbone/core-view');
var Backbone = require('backbone');
var template = require('./layer-header.tpl');
var SyncInfoView = require('./sync-info/sync-info-view');
var ContextMenuView = require('builder/components/context-menu/context-menu-view');
var CustomListCollection = require('builder/components/custom-list/custom-list-collection');
var VisTableModel = require('builder/data/visualization-table-model');
var renameLayer = require('./operations/rename-layer');
var DeleteLayerConfirmationView = require('builder/components/modals/remove-layer/delete-layer-confirmation-view');
var InlineEditorView = require('builder/components/inline-editor/inline-editor-view');
var ModalExportDataView = require('builder/components/modals/export-data/modal-export-data-view');
var templateInlineEditor = require('./inline-editor.tpl');
var zoomToData = require('builder/editor/map-operations/zoom-to-data');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var IconView = require('builder/components/icon/icon-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
const { getSourceNode, nodeHasTradeArea, nodeHasSQLFunction } = require('builder/helpers/analysis-node-utils');

var REQUIRED_OPTS = [
  'modals',
  'userActions',
  'layerDefinitionModel',
  'layerDefinitionsCollection',
  'configModel',
  'stateDefinitionModel',
  'editorModel',
  'userModel',
  'visDefinitionModel',
  'widgetDefinitionsCollection'
];

module.exports = CoreView.extend({
  module: 'layers:layer-header-view',

  className: 'js-editorPanelHeader',

  events: {
    'click .js-toggle-menu': '_onToggleContextMenuClicked',
    'click .js-zoom': '_onZoomClicked',
    'blur .js-input': '_hideRenameInput',
    'keyup .js-input': '_onKeyUpInput'
  },

  initialize: function (opts) {
    checkAndBuildOpts(opts, REQUIRED_OPTS, this);

    this._sourceNodeModel = getSourceNode(this._getNodeModel());
    this._topQueryGeometryModel = null;

    this._initVisTableModel();
    this._initViewState();
    this._bindEvents();
    this._onSourceChanged();
  },

  render: function () {
    this.clearSubViews();

    var tableName = '';
    var url = '';

    if (this._visTableModel) {
      var tableModel = this._visTableModel.getTableModel();
      tableName = tableModel.getUnquotedName();
      url = this._visTableModel && this._visTableModel.datasetURL();
    }

    this.$el.html(
      template({
        letter: this._layerDefinitionModel.get('letter'),
        id: this._getNodeModel().id,
        bgColor: this._layerDefinitionModel.getColor(),
        isTableSource: !!this._sourceNodeModel,
        url: url,
        tableName: tableName,
        title: this._layerDefinitionModel.getTableName().replace(/_/gi, ' '),
        alias: this._layerDefinitionModel.getName(),
        isEmpty: this._viewState.get('isLayerEmpty'),
        canBeGeoreferenced: this._viewState.get('canBeGeoreferenced')
      })
    );

    this._showOrHideZoomVisibility();
    this._initViews();
    this._changeStyle();

    return this;
  },

  _initVisTableModel: function () {
    if (this._sourceNodeModel) {
      var tableName = this._sourceNodeModel.get('table_name');
      this._visTableModel = new VisTableModel({
        id: tableName,
        table: {
          name: tableName
        }
      }, {
        configModel: this._configModel
      });
    }
  },

  _initViewState: function () {
    this._viewState = new Backbone.Model({
      isLayerEmpty: false,
      hasGeom: true,
      canBeGeoreferenced: false
    });
    this._setViewStateValues();
  },

  _initViews: function () {
    this._addSyncInfo();

    this._inlineEditor = new InlineEditorView({
      template: templateInlineEditor,
      renderOptions: {
        alias: this._layerDefinitionModel.getName()
      },
      onEdit: this._renameLayer.bind(this)
    });
    this.addView(this._inlineEditor);
    this.$('.js-header').append(this._inlineEditor.render().el);

    var centerTooltip = new TipsyTooltipView({
      el: this._getZoom(),
      gravity: 'w',
      title: function () {
        return _t('editor.layers.options.center-map');
      }
    });

    this.addView(centerTooltip);

    var toggleMenuTooltip = new TipsyTooltipView({
      el: this._getToggleMenu(),
      gravity: 'w',
      title: function () {
        return _t('more-options');
      }
    });
    this.addView(toggleMenuTooltip);

    if (this._viewState.get('isLayerEmpty') || this._viewState.get('canBeGeoreferenced')) {
      var warningIcon = new IconView({
        placeholder: this.$el.find('.js-warningIcon'),
        icon: 'warning'
      });
      warningIcon.render();
      this.addView(warningIcon);

      var title = this._viewState.get('isLayerEmpty')
        ? _t('editor.layers.layer.empty-layer')
        : _t('editor.layers.layer.geocode-tooltip');

      var emptyLayerTooltip = new TipsyTooltipView({
        el: this.$el.find('.js-warningIcon'),
        gravity: 'w',
        title: function () {
          return title;
        }
      });
      this.addView(emptyLayerTooltip);
    }
  },

  _getNodeModel: function () {
    return this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
  },

  _addSyncInfo: function () {
    var nodeModel = this._getNodeModel();

    if (nodeModel && nodeModel.tableModel && nodeModel.tableModel.isSync()) {
      this._createSyncInfo(nodeModel.tableModel);
    }
  },

  _createSyncInfo: function (tableModel) {
    var syncModel = tableModel.getSyncModel();
    this._syncInfoView = new SyncInfoView({
      modals: this._modals,
      syncModel: tableModel._syncModel,
      tableModel: tableModel,
      userModel: this._userModel,
      visDefinitionModel: this._visDefinitionModel
    });

    this.addView(this._syncInfoView);
    this.$el.prepend(this._syncInfoView.render().el);

    syncModel.bind('destroy', this._destroySyncInfo, this);
    this.add_related_model(syncModel);
  },

  _destroySyncInfo: function () {
    this.removeView(this._syncInfoView);
    this._syncInfoView.clean();
    delete this._syncInfoView;
  },

  _bindEvents: function () {
    if (this._tableNodeModel) {
      this._tableNodeModel.bind('change:synchronization', this.render, this);
      this.add_related_model(this._tableNodeModel);
    }

    this._changeStyle = this._changeStyle.bind(this);
    this._layerDefinitionModel.bind('change:source', this._onSourceChanged, this);
    this._layerDefinitionModel.bind('change:source', this.render, this);
    this._editorModel.on('change:edition', this._changeStyle, this);
    this.add_related_model(this._editorModel);
    this.add_related_model(this._layerDefinitionModel);
    this.listenTo(this._viewState, 'change:hasGeom', this._showOrHideZoomVisibility);
    this.listenTo(this._viewState, 'change:isLayerEmpty change:canBeGeoreferenced', this.render);
  },

  _onSourceChanged: function () {
    var nodeModel = this._getNodeModel();

    if (this._topQueryGeometryModel !== null) {
      this._topQueryGeometryModel.unbind('change:simple_geom');
    }
    this._topQueryGeometryModel = nodeModel.queryGeometryModel.bind('change:simple_geom', this._setViewStateValues, this);
    this._setViewStateValues();
  },

  _changeStyle: function () {
    var editing = this._editorModel.isEditing();
    this._getTitle().toggleClass('u-whiteTextColor', editing);
    this._getText().toggleClass('u-altTextColor', editing);
    this._getInlineEditor().toggleClass('u-mainTextColor', editing);
    this._getLink().toggleClass('u-whiteTextColor', editing);
    this._getBack().toggleClass('u-whiteTextColor', editing);
    this._getToggleMenu().toggleClass('is-white', editing);
    this._getZoom().toggleClass('is-white', editing);
  },

  _getInlineEditor: function () {
    return this.$('.Inline-editor-input');
  },

  _getTitle: function () {
    return this.$('.Editor-HeaderInfo-titleText .js-title');
  },

  _getText: function () {
    return this.$('.Editor-breadcrumbItem');
  },

  _getLink: function () {
    return this.$('.CDB-Text a');
  },

  _getBack: function () {
    return this.$('.js-back');
  },

  _getToggleMenu: function () {
    return this.$('.js-toggle-menu');
  },

  _getZoom: function () {
    return this.$('.js-zoom');
  },

  _onToggleContextMenuClicked: function (event) {
    if (this._hasContextMenu()) {
      this._hideContextMenu();
    } else {
      this._showContextMenu({
        x: event.pageX,
        y: event.pageY
      });
    }
  },

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

  _showContextMenu: function (position) {
    var menuItems = new CustomListCollection([{
      label: _t('editor.layers.options.rename'),
      val: 'rename-layer'
    }, {
      label: this._layerHidden() ? _t('editor.layers.options.show') : _t('editor.layers.options.hide'),
      val: 'toggle-layer'
    }, {
      label: _t('editor.layers.options.export'),
      val: 'export-data'
    }]);
    if (this._layerDefinitionModel.canBeDeletedByUser()) {
      menuItems.add({
        label: _t('editor.layers.options.delete'),
        val: 'delete-layer',
        destructive: true
      });
    }

    var triggerElementID = 'context-menu-trigger-' + this._layerDefinitionModel.cid;
    this._getToggleMenu().attr('id', triggerElementID);
    this._menuView = new ContextMenuView({
      collection: menuItems,
      triggerElementID: triggerElementID,
      position: position
    });

    menuItems.bind('change:selected', function (menuItem) {
      if (menuItem.get('val') === 'delete-layer') {
        this._confirmDeleteLayer();
      }
      if (menuItem.get('val') === 'rename-layer') {
        this._inlineEditor.edit();
      }
      if (menuItem.get('val') === 'toggle-layer') {
        var savingOptions = {
          shouldPreserveAutoStyle: true
        };
        this._layerDefinitionModel.toggleVisible();
        this._userActions.saveLayer(this._layerDefinitionModel, savingOptions);
      }
      if (menuItem.get('val') === 'export-data') {
        this._exportLayer();
      }
    }, this);

    this._menuView.model.bind('change:visible', function (model, isContextMenuVisible) {
      if (this._hasContextMenu() && !isContextMenuVisible) {
        this._hideContextMenu();
      }
    }, this);

    this._menuView.show();
    this.addView(this._menuView);
  },

  _confirmDeleteLayer: function () {
    this._modals.create(function (modalModel) {
      var deleteLayerConfirmationView = new DeleteLayerConfirmationView({
        userActions: this._userActions,
        modals: this._modals,
        layerModel: this._layerDefinitionModel,
        modalModel: modalModel,
        visDefinitionModel: this._visDefinitionModel,
        widgetDefinitionsCollection: this._widgetDefinitionsCollection
      });

      return deleteLayerConfirmationView;
    }.bind(this));
  },

  _hideContextMenu: function () {
    this.removeView(this._menuView);
    this._menuView.clean();
    delete this._menuView;
  },

  _exportLayer: function () {
    var nodeModel = this._getNodeModel();
    const { queryGeometryModel, querySchemaModel } = nodeModel;
    const canHideColumns = nodeHasTradeArea(nodeModel) &&
      !nodeHasSQLFunction(nodeModel);

    this._modals.create(function (modalModel) {
      return new ModalExportDataView({
        fromView: 'layer',
        modalModel: modalModel,
        queryGeometryModel,
        querySchemaModel,
        canHideColumns,
        layerModel: this._layerDefinitionModel,
        configModel: this._configModel,
        filename: this._layerDefinitionModel.getName()
      });
    }.bind(this));
  },

  _renameLayer: function () {
    var newName = this._inlineEditor.getValue();

    if (newName !== '') {
      // Optimistic
      this._onSaveSuccess(newName);

      renameLayer({
        newName: newName,
        userActions: this._userActions,
        layerDefinitionsCollection: this._layerDefinitionsCollection,
        layerDefinitionModel: this._layerDefinitionModel,
        onError: this._onSaveError.bind(this)
      });
    }
  },

  _onSaveSuccess: function (newName) {
    this.$('.js-title').text(newName).show();
    this.$('.js-title-editor').attr('title', newName);
    this._inlineEditor.hide();
  },

  _onSaveError: function (oldName) {
    this.$('.js-title').text(oldName).show();
    this._inlineEditor.hide();
  },

  _layerHidden: function () {
    return this._layerDefinitionModel.get('visible') === false;
  },

  _onZoomClicked: function () {
    var nodeModel = this._getNodeModel();
    var query = nodeModel.querySchemaModel.get('query');
    zoomToData(this._configModel, this._stateDefinitionModel, query);
  },

  _showOrHideZoomVisibility: function () {
    this._getZoom().toggle(this._viewState.get('hasGeom'));
  },

  _setViewStateValues: function () {
    var nodeModel = this._getNodeModel();
    var isEmptyPromise = this._layerDefinitionModel.isEmptyAsync();
    var hasGeomPromise = nodeModel.queryGeometryModel.hasValueAsync();
    var canBeGeoreferencedPromise = this._layerDefinitionModel.canBeGeoreferenced();

    Promise.all([isEmptyPromise, hasGeomPromise, canBeGeoreferencedPromise])
      .then(function (values) {
        this._viewState.set({
          isLayerEmpty: values[0],
          hasGeom: values[1],
          canBeGeoreferenced: values[2]
        });
      }.bind(this));
  }
});