CartoDB/cartodb20

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

Summary

Maintainability
B
4 hrs
Test Coverage
var _ = require('underscore');
var $ = require('jquery');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var queue = require('queue-async');
var EditorWidgetView = require('./widget-view');
var IconView = require('builder/components/icon/icon-view');
var template = require('./widgets-view.tpl');
var widgetPlaceholderTemplate = require('./widgets-placeholder.tpl');
var widgetsErrorTemplate = require('./widgets-content-error.tpl');
var widgetsNotReadyTemplate = require('./widgets-content-not-ready.tpl');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var AddWidgetsView = require('builder/components/modals/add-widgets/add-widgets-view');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');

require('jquery-ui');

var STATES = {
  loading: 'loading',
  ready: 'ready'
};

var REQUIRED_OPTS = [
  'userActions',
  'analysisDefinitionNodesCollection',
  'layerDefinitionsCollection',
  'widgetDefinitionsCollection',
  'userModel',
  'stackLayoutModel',
  'configModel',
  'modals'
];

var SAVE_WIDGET_BATCH_SIZE = 5;

/**
 * View to render widgets definitions overview
 */
module.exports = CoreView.extend({

  module: 'editor:widgets:widgets-view',

  events: {
    'click .js-add': '_addWidget'
  },

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

    this.viewModel = new Backbone.Model({
      state: STATES.loading
    });

    this._initViewState();
    this._initBinds();

    var callback = this._onAllQueryGeometryLoaded.bind(this);
    this._layerDefinitionsCollection.loadAllQueryGeometryModels(callback);
  },

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

    this._initViews();

    return this;
  },

  _initViewState: function () {
    this._viewState = new Backbone.Model({
      anyGeometryData: true
    });

    this._setViewState();
  },

  _initViews: function () {
    if (this._isLoading()) {
      this.$el.append(widgetsNotReadyTemplate());
      return;
    }

    if (!this._viewState.get('anyGeometryData')) {
      this._showNoGeometryData();
      return;
    }

    if (this._widgetDefinitionsCollection.size() > 0) {
      this.$el.append(template);
      _.each(this._widgetDefinitionsCollection.sortBy('order'), this._addWidgetItem, this);
      this._initSortable();
      this._addTooltip();
      this._renderPlusIcon();
      return;
    }

    this.$el.append(widgetPlaceholderTemplate());
    this._addTooltip();
    this._renderPlusIcon();
  },

  _renderPlusIcon: function () {
    var plusIcon = new IconView({
      placeholder: this.$el.find('.js-plus-icon'),
      icon: 'plus'
    });
    plusIcon.render();
    this.addView(plusIcon);
  },

  _initBinds: function () {
    this.listenTo(this._widgetDefinitionsCollection, 'destroy successAdd', this.render, this);
    this.listenTo(this.viewModel, 'change:state', this.render, this);
    this.listenTo(this._viewState, 'change', this.render);
  },

  _showNoGeometryData: function () {
    this.$el.append(
      widgetsErrorTemplate({
        body: _t('editor.widgets.no-geometry-data')
      })
    );
  },

  _onAllQueryGeometryLoaded: function () {
    this.viewModel.set('state', STATES.ready);
  },

  _addTooltip: function () {
    var tooltip = new TipsyTooltipView({
      el: this.$('.js-add'),
      gravity: 'w',
      title: function () {
        return _t('editor.widgets.add-widget.tooltip');
      },
      offset: 8
    });

    this.addView(tooltip);
  },

  _isLoading: function () {
    return this.viewModel.get('state') === STATES.loading;
  },

  _isReady: function () {
    return this.viewModel.get('state') === STATES.ready;
  },

  _setViewState: function () {
    this._layerDefinitionsCollection.isThereAnyGeometryData()
      .then(function (anyGeometry) {
        this._viewState.set('anyGeometryData', anyGeometry);
      }.bind(this));
  },

  _initSortable: function () {
    this.$('.js-widgets').sortable({
      axis: 'y',
      items: '> li.BlockList-item',
      opacity: 0.8,
      update: this._onSortableFinish.bind(this),
      forcePlaceholderSize: false
    }).disableSelection();
  },

  _destroySortable: function () {
    if (this.$('.js-widgets').data('ui-sortable')) {
      this.$('.js-widgets').sortable('destroy');
    }
  },

  _onSortableFinish: async function () {
    var self = this;
    var savedLayers = {};

    var widgets = this.$('.js-widgets > .js-widgetItem').map(function (index, item) {
      var modelCid = $(item).data('model-cid');
      var widgetDefModel = self._widgetDefinitionsCollection.get(modelCid);
      widgetDefModel.set('order', index);

      // NOTE: Save only each layer analysis once to avoid rate limits
      var layerId = widgetDefModel.get('layer_id');
      var data = { model: widgetDefModel, saveLayer: !savedLayers[layerId] };
      savedLayers[layerId] = true;

      return data;
    });

    var q = queue(SAVE_WIDGET_BATCH_SIZE);
    _.each(widgets, function (item) {
      q.defer(async function (callback) {
        await self._userActions.saveWidget(item.model, item.saveLayer, true);
        callback();
      });
    });
  },

  _addWidget: function () {
    if (this.$('.js-add').hasClass('is-disabled')) return;

    var self = this;

    this._modals.create(function (modalModel) {
      return new AddWidgetsView({
        modalModel: modalModel,
        userModel: self._userModel,
        userActions: self._userActions,
        configModel: self._configModel,
        analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection,
        layerDefinitionsCollection: self._layerDefinitionsCollection,
        widgetDefinitionsCollection: self._widgetDefinitionsCollection
      });
    }, {
      breadcrumbsEnabled: true
    });
  },

  _addWidgetItem: function (model) {
    var view = new EditorWidgetView({
      model: model,
      layer: this._layerDefinitionsCollection.get(model.get('layer_id')),
      modals: this._modals,
      userActions: this._userActions,
      stackLayoutModel: this._stackLayoutModel
    });
    this.addView(view);
    this.$('.js-widgets').append(view.render().el);
  },

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