CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/builder/editor/style/style-view.js

Summary

Maintainability
F
5 days
Test Coverage
var _ = require('underscore');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var PanelWithOptionsView = require('builder/components/view-options/panel-with-options-view');
var StyleContentView = require('./style-content-view');
var StyleCartoCSSView = require('./style-cartocss-view');
var ScrollView = require('builder/components/scroll/scroll-view');
var TabPaneView = require('builder/components/tab-pane/tab-pane-view');
var TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
var Toggler = require('builder/components/toggler/toggler-view');
var UndoButtons = require('builder/components/undo-redo/undo-redo-view');
var ParserCSS = require('builder/helpers/parser-css');
var Infobox = require('builder/components/infobox/infobox-factory');
var InfoboxModel = require('builder/components/infobox/infobox-model');
var InfoboxCollection = require('builder/components/infobox/infobox-collection');
var Notifier = require('builder/components/notifier/notifier');
var CartoCSSNotifications = require('builder/cartocss-notifications');
var MetricsTracker = require('builder/components/metrics/metrics-tracker');
var MetricsTypes = require('builder/components/metrics/metrics-types');

var OnboardingLauncher = require('builder/components/onboardings/generic/generic-onboarding-launcher');
var OnboardingView = require('builder/components/onboardings/layers/style-onboarding/style-onboarding-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');

var ONBOARDING_KEY = 'layer-style-onboarding';

var REQUIRED_OPTS = [
  'layerDefinitionsCollection',
  'layerDefinitionModel',
  'userActions',
  'queryGeometryModel',
  'querySchemaModel',
  'queryRowsCollection',
  'modals',
  'editorModel',
  'configModel',
  'userModel',
  'onboardings',
  'onboardingNotification',
  'layerContentModel'
];

module.exports = CoreView.extend({

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

    this._styleModel = this._layerDefinitionModel.styleModel;
    this._cartocssModel = this._layerDefinitionModel.cartocssModel;
    this._codemirrorModel = new Backbone.Model({
      content: this._layerDefinitionModel.get('cartocss')
    });

    // Set edition attribute in case custom cartocss is applied
    this._editorModel.set({
      edition: !!this._layerDefinitionModel.get('cartocss_custom'),
      disabled: false
    });

    this._infoboxModel = new InfoboxModel({
      state: this._isLayerHidden() ? 'layer-hidden' : ''
    });

    this._overlayModel = new Backbone.Model({
      visible: this._isLayerHidden()
    });

    this._applyButtonStatusModel = new Backbone.Model({
      loading: false
    });

    this._togglerModel = new Backbone.Model({
      labels: [_t('editor.style.style-toggle.values'), _t('editor.style.style-toggle.cartocss')],
      active: this._editorModel.isEditing(),
      disabled: this._editorModel.isDisabled(),
      isDisableable: true,
      tooltip: _t('editor.style.style-toggle.tooltip')
    });

    this._appendMapsAPIError();

    CartoCSSNotifications.track(this);

    this._onboardingLauncher = new OnboardingLauncher({
      view: OnboardingView,
      onboardingNotification: this._onboardingNotification,
      notificationKey: ONBOARDING_KEY,
      onboardings: this._onboardings
    }, {
      editorModel: this._editorModel,
      selector: 'LayerOnboarding'
    });

    this._configPanes();
    this._initBinds();
  },

  render: function () {
    this._launchOnboarding();
    this.clearSubViews();
    this.$el.empty();

    this._initViews();
    this._infoboxState();

    return this;
  },

  _launchOnboarding: function () {
    if (this._onboardingNotification.getKey(ONBOARDING_KEY)) {
      return;
    }

    var georeferencePromise = this._layerDefinitionModel.canBeGeoreferenced();
    var hasGeomPromise = this._queryGeometryModel.hasValueAsync();
    var launchOnboarding = function (canBeGeoreferenced, hasGeom) {
      if (!this._editorModel.isEditing() && !canBeGeoreferenced && hasGeom) {
        this._onboardingLauncher.launch({
          geom: this._queryGeometryModel.get('simple_geom'),
          type: this._styleModel.get('type')
        });
      }
    }.bind(this);

    Promise.all([georeferencePromise, hasGeomPromise])
      .then(function (values) {
        launchOnboarding(values[0], values[1]);
      });
  },

  _initBinds: function () {
    this.listenTo(this._layerDefinitionModel, 'change:error', this._appendMapsAPIError);
    this.listenTo(this._layerDefinitionModel, 'change:cartocss', this._onCartocssChanged);
    this.listenTo(this._layerDefinitionModel, 'change:visible', this._infoboxState);
    this.listenTo(this._layerDefinitionModel, 'change:autoStyle', this._infoboxState);

    this.listenTo(this._editorModel, 'change:edition', this._onChangeEdition);
    this.listenTo(this._editorModel, 'change:disabled', this._onChangeDisabled);
    this.listenTo(this._togglerModel, 'change:active', this._onTogglerChanged);

    this.listenTo(this._querySchemaModel, 'change:query_errors', this._updateEditor);

    this.listenTo(this._styleModel, 'change', this._onCartocssChanged);
    this.listenTo(this._cartocssModel, 'undo redo', this._onUndoRedo);
  },

  _initViews: function () {
    var self = this;

    var infoboxSstates = [
      {
        state: 'confirm',
        createContentView: function () {
          return Infobox.createWithAction({
            type: 'alert',
            title: _t('editor.style.messages.cartocss-applied.title'),
            body: _t('editor.style.messages.cartocss-applied.body'),
            action: {
              label: _t('editor.style.messages.cartocss-applied.clear')
            }
          });
        },
        onAction: self._clearCustomStyles.bind(self),
        onClose: self._cancelClearStyles.bind(self)
      }, {
        state: 'layer-hidden',
        createContentView: function () {
          return Infobox.createWithAction({
            type: self._layerDefinitionModel.get('cartocss_custom') ? 'code' : 'alert',
            title: _t('editor.messages.layer-hidden.title'),
            body: _t('editor.messages.layer-hidden.body'),
            action: {
              label: _t('editor.messages.layer-hidden.show')
            }
          });
        },
        onAction: self._showHiddenLayer.bind(self)
      }, {
        state: 'torque-exists',
        createContentView: function () {
          return Infobox.createWithAction({
            type: 'alert',
            title: _t('editor.style.messages.torque-exists.title'),
            body: _t('editor.style.messages.torque-exists.body'),
            action: {
              label: _t('editor.style.messages.torque-exists.continue')
            }
          });
        },
        onAction: self._applyTorqueAggregation.bind(self),
        onClose: self._cancelTorqueAggregation.bind(self)
      }
    ];

    var infoboxCollection = new InfoboxCollection(infoboxSstates);

    var panelWithOptionsView = new PanelWithOptionsView({
      className: 'Editor-content',
      editorModel: self._editorModel,
      infoboxModel: self._infoboxModel,
      infoboxCollection: infoboxCollection,
      createContentView: function () {
        return new TabPaneView({
          collection: self._collectionPane
        });
      },
      createControlView: function () {
        return new Toggler({
          model: self._togglerModel
        });
      },
      createActionView: function () {
        return new TabPaneView({
          collection: self._collectionPane,
          createContentKey: 'createActionView'
        });
      }
    });

    this.$el.append(panelWithOptionsView.render().el);
    this.addView(panelWithOptionsView);
  },

  _onCartocssChanged: function () {
    this._codemirrorModel.set('content', this._layerDefinitionModel.get('cartocss'));
  },

  _onUndoRedo: function () {
    this._codemirrorModel.set('content', this._cartocssModel.get('content'));
  },

  _appendMapsAPIError: function () {
    var error = this._layerDefinitionModel.get('error');
    if (error) {
      if (error.subtype === 'turbo-carto') {
        var newErrors = _.clone(this._codemirrorModel.get('errors')) || [];
        newErrors.push({ line: error.line, message: error.message });
        this._codemirrorModel.set('errors', newErrors);
      }
    }
  },

  _updateEditor: function (model) {
    var errors = this._querySchemaModel.get('query_errors');
    var hasErrors = errors && errors.length > 0;
    this._editorModel.set('disabled', hasErrors);
  },

  _saveCartoCSS: function (cb) {
    var content = this._codemirrorModel.get('content');
    var parser = new ParserCSS(content);
    var errors = parser.errors();

    if (!content) {
      return false;
    }

    this._applyButtonStatusModel.set('loading', true);
    this._codemirrorModel.set('errors', parser.parseError(errors));

    if (errors.length === 0) {
      this._cartocssModel.set('content', content);

      // Disable auto-style before saving, in order to not reset styles
      this._layerDefinitionModel.set('autoStyle', false);

      this._layerDefinitionModel.save({
        cartocss_custom: true,
        cartocss: content
      }, {
        complete: cb
      });

      MetricsTracker.track(MetricsTypes.APPLIED_CARTOCSS, {
        layer_id: this._layerDefinitionModel.get('id'),
        cartocss: this._layerDefinitionModel.get('cartocss')
      });

      MetricsTracker.track(MetricsTypes.USED_ADVANCED_MODE, {
        mode_type: 'cartocss'
      });
    } else {
      this._editorModel.get('edition') === false && CartoCSSNotifications.showErrorNotification(parser.parseError(errors));
      this._applyButtonStatusModel.set('loading', false);
    }
  },

  _onSaveComplete: function () {
    CartoCSSNotifications.showSuccessNotification();
    this._applyButtonStatusModel.set('loading', false);
  },

  _cancelClearStyles: function () {
    this._infoboxModel.set({ state: '' });
    this._overlayModel.set({ visible: false });

    this._editorModel.set({
      edition: true
    });
  },

  _clearCustomStyles: function () {
    this._layerDefinitionModel.set('cartocss_custom', false);
    this._styleModel.applyLastState();
    this._infoboxModel.set({ state: '' });
    this._overlayModel.set({ visible: false });
    this.render();
  },

  _cancelTorqueAggregation: function () {
    this._styleModel.applyLastState();
    this._infoboxModel.set({ state: '' });
    this._overlayModel.set({ visible: false });
    this.render();
  },

  _applyTorqueAggregation: function () {
    if (this._layerDefinitionsCollection.isThereAnyTorqueLayer()) {
      var torqueLayer = this._layerDefinitionsCollection.findWhere({ type: 'torque' });
      torqueLayer.styleModel.setDefaultPropertiesByType('simple', 'point');
    }

    this._infoboxModel.set({ state: 'unfreeze' });
  },

  _onChangeEdition: function () {
    this._infoboxState();

    var edition = this._editorModel.get('edition');
    var index = edition ? 1 : 0;
    this._collectionPane.at(index).set({ selected: true });
    this._togglerModel.set({ active: edition });
  },

  _onChangeDisabled: function () {
    var disabled = this._editorModel.get('disabled');
    this._togglerModel.set({ disabled: disabled });
  },

  _onTogglerChanged: function () {
    var checked = this._togglerModel.get('active');
    this._editorModel.set({ edition: checked });
  },

  _freezeTorgeAggregation: function (styleType, currentGeometryType) {
    this._infoboxModel.set({ state: 'torque-exists' });
    this._overlayModel.set({ visible: true });

    // Apply the previously selected style
    this._infoboxModel.once('change:state', function (mdl, state) {
      if (state === 'unfreeze') {
        this._moveTorqueLayerToTop(function () {
          this._overlayModel.set({ visible: false });
          this._styleModel.setDefaultPropertiesByType(styleType, currentGeometryType);
        }.bind(this));
        this._infoboxModel.set({ state: '' });
      }
    }, this);
  },

  _moveTorqueLayerToTop: function (callback) {
    var notification = Notifier.addNotification({
      status: 'loading',
      info: _t('editor.layers.moveTorqueLayer.loading'),
      closable: true
    });

    this._layerDefinitionsCollection.once('layerMoved', function () {
      callback && callback();
      notification.set({
        status: 'success',
        info: _t('editor.layers.moveTorqueLayer.success'),
        delay: Notifier.DEFAULT_DELAY
      });
    }, this);

    this._userActions.moveLayer({
      from: this._layerDefinitionModel.get('order'),
      to: this._layerDefinitionsCollection.getTopDataLayerIndex()
    });
  },

  _configPanes: function () {
    var self = this;
    var tabPaneTabs = [{
      selected: !this._layerDefinitionModel.get('cartocss_custom'),
      createContentView: function () {
        return new ScrollView({
          createContentView: function () {
            return new StyleContentView({
              className: 'Editor-content',
              userActions: self._userActions,
              layerDefinitionsCollection: self._layerDefinitionsCollection,
              layerDefinitionModel: self._layerDefinitionModel,
              styleModel: self._styleModel,
              modals: self._modals,
              configModel: self._configModel,
              userModel: self._userModel,
              queryGeometryModel: self._queryGeometryModel,
              querySchemaModel: self._querySchemaModel,
              editorModel: self._editorModel,
              overlayModel: self._overlayModel,
              freezeTorgeAggregation: self._freezeTorgeAggregation.bind(self),
              layerContentModel: self._layerContentModel
            });
          }
        });
      },
      createActionView: function () {
        return new UndoButtons({
          trackModel: self._styleModel,
          editorModel: self._editorModel,
          applyButton: false
        });
      }
    }, {
      selected: this._layerDefinitionModel.get('cartocss_custom'),
      createContentView: function () {
        return new StyleCartoCSSView({
          layerDefinitionModel: self._layerDefinitionModel,
          querySchemaModel: self._querySchemaModel,
          styleModel: self._styleModel,
          editorModel: self._editorModel,
          codemirrorModel: self._codemirrorModel,
          onApplyEvent: self._saveCartoCSS.bind(self, self._onSaveComplete.bind(self)),
          overlayModel: self._overlayModel
        });
      },
      createActionView: function () {
        return new UndoButtons({
          trackModel: self._cartocssModel,
          editorModel: self._editorModel,
          applyStatusModel: self._applyButtonStatusModel,
          applyButton: true,
          onApplyClick: self._saveCartoCSS.bind(self, self._onSaveComplete.bind(self)),
          overlayModel: self._overlayModel
        });
      }
    }];

    this._collectionPane = new TabPaneCollection(tabPaneTabs);
  },

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

  _infoboxState: function () {
    var edition = this._editorModel.get('edition');
    var cartocss_custom = this._layerDefinitionModel.get('cartocss_custom');
    var isAutoStyleApplied = this._layerDefinitionModel.get('autoStyle');

    if (!edition && cartocss_custom && !isAutoStyleApplied) {
      this._infoboxModel.set({ state: 'confirm' });
      this._overlayModel.set({ visible: true });
    } else if (this._isLayerHidden()) {
      this._infoboxModel.set({ state: 'layer-hidden' });
      this._overlayModel.set({ visible: true });
      this._togglerModel.set({ disabled: true });
    } else {
      this._infoboxModel.set({ state: '' });
      this._overlayModel.set({ visible: false });
      this._togglerModel.set({ disabled: false });
    }
  },

  _showHiddenLayer: function () {
    var savingOptions = {
      shouldPreserveAutoStyle: true
    };
    this._layerDefinitionModel.toggleVisible();
    this._userActions.saveLayer(this._layerDefinitionModel, savingOptions);
  }
});