CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/cartodb/table/overlays/annotation.js

Summary

Maintainability
F
1 wk
Test Coverage
cdb.admin.overlays.Annotation = cdb.geo.ui.Annotation.extend({

  className: "annotation overlay",

  template_name: 'table/views/overlays/annotation',

  events: {

    "mouseenter .text":   "_onMouseEnter",
    "mouseup":            "_onMouseUp",

    "click .close":      "_close",
    "click .content":    "_onClickEdit",
    "click .text":       "_onClickEdit",
    "dblclick .content": "_onDblClick",
    "dblclick .text":    "_onDblClick",

    "keyup .text":       "_onKeyUp",
    "paste .text":       "_onPaste"

  },

  initialize: function() {

    _.bindAll(this, "_close", "_onChangeMode", "_onKeyDown");

    this.vis    = this.options.vis;
    this.canvas = this.options.canvas;

    this.mapView = this.options.mapView;

    this._bindMap();

    this.template = this.getTemplate(this.template_name);

    this._setupModels();

    // zoom level config
    var minZoomLevel     = this.mapView.map.get("minZoom");
    var maxZoomLevel     = this.mapView.map.get("maxZoom");

    this.form_data = [{
        name: 'Text',
        form: {
          'font-size':  { type: 'simple_number', value: 12, min: 5, max: 50, inc: 2, disable_triggering: true },
          'color':      { type: 'color', value: '#FFF', extra: { tick: "left", picker_horizontal_position: "left", picker_vertical_position: "down" }},
          'font-family-name': {
            type: 'select',
            value: "Helvetica",
            extra: ["Helvetica", "Droid Sans", "Vollkorn", "Roboto", "Open Sans", "Lato", "Graduate", "Gravitas One", "Old Standard TT"]
          },
          'text-align':      { type: 'text_align', value: 'left', alignments: { left: true, right: true, center: false } },
        }
      }, {
        name: 'Box',
        form: {
          'box-color':  { type: 'color', value: '#000', extra: { tick: "left", picker_horizontal_position: "left", picker_vertical_position: "down" }},
          'box-opacity':  { type: 'simple_opacity', value: .7, min:0, max:1, inc: .1, disable_triggering: true },
          'box-padding':  { type: 'simple_number_with_label', value: 5, min: 5, max: 50, inc: 1, label: "P", disable_triggering: true }
        }
      }, {
        name: 'Line',
        form: {
          'line-color':  { type: 'color', value: '#000', extra: { tick: "left", picker_horizontal_position: "left", picker_vertical_position: "down" }},
          'line-width':  { type: 'simple_number_with_label', value: 50, min: 5, max: 100, inc: 1, label: 'W', disable_triggering: true },
        }
      } , {
        name: 'Zoom (min-max)',
        form: {
          'min-zoom':  { type: 'simple_number', value: minZoomLevel, min: minZoomLevel, max: maxZoomLevel, inc: 1, classes: "margin-min", disable_triggering: true },
          'max-zoom':  { type: 'simple_number_with_label', value: maxZoomLevel, min: minZoomLevel, max: maxZoomLevel, inc: 1, label: '↔', disable_triggering: true },
        }
      }];
  },

  _bindMap: function() {

    this.mapView.map.bind('change',      this._place, this);
    this.mapView.map.bind('change:zoom', this._applyZoomLevelStyle, this);
    this.mapView.bind('zoomstart', this._hideOverlay, this);
    this.mapView.bind('zoomend',   this._showOverlay, this);

  },

  _unbindMap: function() {

    this.mapView.map.unbind('change',      this._place, this);
    this.mapView.map.unbind('change:zoom', this._applyZoomLevelStyle, this);
    this.mapView.unbind('zoomstart', this._hideOverlay, this);
    this.mapView.unbind('zoomend',   this._showOverlay, this);

  },

  // Setup the internal and custom model
  _setupModels: function() {

    var self  = this;
    var extra = this.extra = this.model.get("extra");

    this.model.set({ text: extra.text }, { silent: true });

    var applyStyle = function() {
      self._applyStyle();
      self.model.save();
    };

    // Binding
    this.model.bind('remove',          this.hide,             this);

    this.model.bind('change:style',    applyStyle,            this);
    this.model.bind('change:text',     this._setText,         this);
    this.model.bind('change:display',  this._onChangeDisplay, this);
    this.model.bind('change:extra',    this._onChangeExtra,   this);
    this.model.bind('change:selected', this._onChangeSelected, this);

    // Internal model to store the editing state
    this.editModel = new cdb.core.Model({ mode: "" });
    this.editModel.bind('change:mode', this._onChangeMode, this);

    this.add_related_model(this.editModel);

  },

  // Element events 
  _onKeyUp: function(e) {

    if (this.timeout) {
      clearTimeout(this.timeout);
    }

    this.model.set({ text: this.$text.html() }, { silent: true });

  },

  _onClickEdit: function(e) {

    this.killEvent(e);

    $(document).bind('keydown', this._onKeyDown);

    this.trigger("clickEdit", this.model, this.form_data);
    this.model.set("selected", true);

  },

  _onKeyDown: function(e) {

    var selected = this.model.get("selected");

    if (selected) {

      var editable = this.editModel.get("mode") !== "editable";
      var focus    = this.$(".overlay_text").is(":focus");

      // hitting the backspace removes the overlay
      if ($(e.target).hasClass("overlay_text") || $(e.target).hasClass("cartodb-map")) {
        if (e.keyCode === $.ui.keyCode.BACKSPACE && (editable || !focus)) {
          this.killEvent(e);
          this._close();
        }
      }

      if (editable && !focus) {
        if (e.which == 67 && (e.ctrlKey || e.metaKey)) {
          this.trigger('duplicate', this.model, this);
        }
      }
    }

    if (e.keyCode === $.ui.keyCode.ESCAPE) { 
      this.editModel.set("mode", "");
    }

  },

  _onPaste: function(e) {

    var self = this;

    setTimeout(function() {

      var text = cdb.Utils.stripHTML(self.model.get("text"));
      self.model.set("text", text)

    }, 200);

  },

  _onDblClick: function(e) {

    this.killEvent(e);
    this.editModel.set("mode", "editable");
  },

  _onMouseUp: function() {

    var editable = (this.editModel.get("mode") == "editable");

    if (!editable) {
      this._savePosition();
    }

  },

  _savePosition: function() {

    var extra = this.model.get("extra");

    var x     = this.model.get("x");
    var y     = this.model.get("y");

    var oldX   = this.$el.position().left;
    var oldY   = this.$el.position().top;
    var height = this.$el.height();

    if (x == oldX && y == oldY) return;

    var x = this.$el.position().left;
    var y = this.$el.position().top;

    var style      = this.model.get("style");

    var lineWidth  = style["line-width"];
    var textAlign  = style["text-align"];

    y = y + Math.ceil(this.$el.outerHeight(true)/2);

    if (textAlign === "right") {
      x = x + this.$el.width() + lineWidth + this.$(".ball").width();
    } else {
      x = x - lineWidth;
    }

    var latlng = this.mapView.pixelToLatLon([x , y]);
    extra.latlng = [latlng.lat, latlng.lng];

    if (!this.model.isNew()) { // avoid saving it if the model was removed
      this.model.save({ extra: extra });
    }

  },

  _onMouseDown: function() {},

  _onMouseEnter: function() {

    this.$el.addClass("hover");

    if (this.editModel.get("mode") === "editable") {
      if (this.timeout) clearTimeout(this.timeout);
    }

  },

  _onMouseLeave: function() {
    this.$el.removeClass("hover");

    var self = this;

    if (this.editModel.get("mode") === "editable") {

      this.timeout = setTimeout(function() {

        self.editModel.set("mode", "");

      }, 250);
    }

  },

  _hideOverlay: function() {
    this.$el.fadeOut(150);
  },

  _showOverlay: function() {

    if (!this._belongsToCanvas()) return;

    var self = this;

    this.$el.stop().delay(500).fadeIn(150, function() {
      self.$el.css({ display: "inline-table" }); // trick so we don't need to set the width
    });

  },

  // canonical show method for the overlay
  show: function(animated) {

    if (!this._belongsToCanvas()) return;

    this.$el.show();
    this.$el.css({ display: "inline-table "});

    if (true) this.$el.addClass('animated bounceIn');

    var self = this;

    this.$el.one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() {
      self.$el.removeClass("animated bounceIn");
    });

  },

  // canonical hide (= destroy) method for the overlay
  hide: function(callback) {

    var self = this;

    this.unbind("mouseenter", this._onMouseEnter, this);
    this.unbind("mouseup", this._onMouseUp, this);
    $(document).unbind('keydown', this._onKeyDown, this);

    this.$el
    .removeClass('animated bounceIn')
    .addClass('animated bounceOut')

    callback && _.isFunction(callback) && callback();

    this._unbindMap();

    cdb.god.unbind("closeDialogs", this._onCloseDialogs, this);

    // Give it some time to complete the animation
    setTimeout(function() {
      self.clean();
    }, 550);

  },

  _close: function(e) {

    this.killEvent(e);

    var self = this;

    this.hide(function() {
      self.trigger("remove", self);
    });

  },

  _place: function() {

    var mode = this.editModel.get("mode");

    if (mode === "editable") return;

    var style      = this.model.get("style");
    var lineWidth  = style["line-width"];
    var textAlign  = style["text-align"];

    var pos        = this.mapView.latLonToPixel(this.model.get('extra').latlng);
    var size       = this.mapView.getSize();
    var top        = pos.y - Math.ceil(this.$el.outerHeight(true)/2);
    var left       = pos.x + lineWidth;

    if (textAlign === "right") {
      left = pos.x - this.$el.width() - lineWidth - this.$(".ball").width();
    }

    this.$el.css({ top: top, left: left });

  },

  _belongsToCanvas: function() {

    return this.model.get("device") === this.canvas.get("mode");

  },

  _onChangeDisplay: function() {

    var display = this.model.get("display");

    if (display && this._belongsToCanvas()) {
      this.show();
    } else {
      this._hideOverlay();
    }

  },

  _onChangeExtra: function() {

    var extra  = this.model.get("extra");
    extra.text = this.model.get("text");

    this.model.set({ extra: extra }, { silent: true });

  },

  _getStyleProperty: function(property) {

    var style      = this.model.get("style");

    return style[property];
  },

  /*
   * Applies style to the content of the widget
   */

  _applyStyle: function() {

    var style      = this.model.get("style");

    var textAlign  = style["text-align"];
    var boxColor   = style["box-color"];
    var boxOpacity = style["box-opacity"];
    var boxPadding = style["box-padding"];

    var lineWidth  = style["line-width"];
    var lineColor  = style["line-color"];
    var fontFamily = style["font-family-name"];

    if (boxOpacity === 0) {
      this.$el.addClass("border-dark");
    } else {
      this.$el.removeClass("border-dark");
    }

    if (boxColor === "#FFFFFF") {
      this.$el.addClass("white-box");
    } else {
      this.$el.removeClass("white-box");
    }

    this.$text.css(style);

    this.$(".content").css("padding", boxPadding);
    this.$text.css("font-size", style["font-size"] + "px");
    this.$el.css("z-index", style["z-index"]);

    this.$(".stick").css({ width: lineWidth, left: -lineWidth });

    var fontFamilyClass = "";

    if      (fontFamily  == "Droid Sans")       fontFamilyClass = "droid";
    else if (fontFamily  == "Vollkorn")         fontFamilyClass = "vollkorn";
    else if (fontFamily  == "Open Sans")        fontFamilyClass = "open_sans";
    else if (fontFamily  == "Roboto")           fontFamilyClass = "roboto";
    else if (fontFamily  == "Lato")             fontFamilyClass = "lato";
    else if (fontFamily  == "Graduate")         fontFamilyClass = "graduate";
    else if (fontFamily  == "Gravitas One")     fontFamilyClass = "gravitas_one";
    else if (fontFamily  == "Old Standard TT")  fontFamilyClass = "old_standard_tt";

    this.$el
    .removeClass("droid")
    .removeClass("vollkorn")
    .removeClass("roboto")
    .removeClass("open_sans")
    .removeClass("lato")
    .removeClass("graduate")
    .removeClass("gravitas_one")
    .removeClass("old_standard_tt");

    this.$el.addClass(fontFamilyClass);

    if (textAlign === "right") {
      this.$el.addClass("align-right");
      this.$(".stick").css({ left: "auto", right: -lineWidth });
    } else {
      this.$el.removeClass("align-right");
    }

    this._place();
    this._applyZoomLevelStyle();

  },

  _applyZoomLevelStyle: function() {

    var style      = this.model.get("style");
    var extra      = this.model.get("extra");

    var boxColor   = style["box-color"];
    var boxOpacity = style["box-opacity"];
    var lineColor  = style["line-color"];

    var minZoom    = style["min-zoom"];
    var maxZoom    = style["max-zoom"];

    var currentZoom = this.mapView.map.get("zoom");

    var textOpacity = 1;

    if (currentZoom >= minZoom && currentZoom <= maxZoom) {

      textOpacity = 1;

      var rgbaLineCol = this._getRGBA(lineColor, 1);
      var rgbaBoxCol  = this._getRGBA(boxColor, boxOpacity);

    } else {

      textOpacity = .5;

      var rgbaLineCol = this._getRGBA(lineColor, .2);
      var rgbaBoxCol  = this._getRGBA(boxColor, .2);

    }

    this.$(".text").animate({ opacity: textOpacity }, 150);

    this.$el.css("background-color", rgbaBoxCol);

    this.$(".stick").css("background-color", rgbaLineCol);
    this.$(".ball").css("background-color", rgbaLineCol);

  },

  _onChangeSelected: function() {

    var selected = this.model.get("selected");

    if (selected) {

      this.$el.addClass("selected");

      if (this._getStyleProperty("box-opacity") === 0) {
        this.$el.addClass("border-dark");
      }

      if (this._getStyleProperty("box-color") === "#FFFFFF") {
        this.$el.addClass("white-box");
      }

    } else {

      this.$el
      .removeClass("selected")
      .removeClass("border-dark")
      .removeClass("white-box");

      this._disableEditingMode();

    }

  },

  _onChangeMode: function() {

    var mode = this.editModel.get("mode");

    this.trigger('editing', mode === 'editable', this);

    if (mode == "editable") {
      this._enableEditingMode();
    } else {
      this._disableEditingMode();
    }
  },

  _enableEditingMode: function() {

    this.$el
    .addClass("editable")
    .addClass("disabled");

    this.$text.attr("contenteditable", true).focus();

    var style = this.model.get("style");
    var width = style["box-width"];

    var text = this.model.get("text");

    this.$el.css("width", "auto");
    this.$el.css("max-width", width);
    this.$text.html(text);
    this.$(".hint").fadeIn(150);
  },

  _disableEditingMode: function() {

    $(document).unbind('keydown', this._onKeyDown);

    var text = this._transformToMarkdown(this.model.get("text"));

    this.editModel.set("mode", "");

    if (text) {

      var self = this;

      self.$(".hint").fadeOut(150, function() {

        self.$el
        .removeClass("editable")
        .removeClass("disabled");

        self.$text.attr("contenteditable", false);

      });

      self.$text.html(text);
      self._savePosition();

    } else {
      this._close();
    }

  },

  _setText: function() {

    var text          = this.model.get("text");
    var rendered_text = this._transformToMarkdown(text);

    var extra = this.model.get("extra");

    extra.text          = text;
    extra.rendered_text = rendered_text

    this.model.save({ extra: extra });

    if (rendered_text) this.$text.html(rendered_text);

  },

  _transformToMarkdown: function(text) {

    text = markdown.toHTML(text)

    text = text.replace(/&lt;/g, "<");
    text = text.replace(/&gt;/g, ">");
    text = text.replace(/<p>/g, "");
    text = text.replace(/&amp;nbsp;/g, " ");
    text = text.replace(/<\/p>/g, "");

    return text;

  },

  _onCloseDialogs: function() {
    if (this.model.get("selected") !== undefined) this.model.set("selected", false);
  },

  _setupText: function() {

    this.$text = this.$(".content div.text");
    this.$text.html(this._transformToMarkdown(this.model.get("text")));

  },

  render: function() {

    this.$el.append(this.template(this.model.attributes));
    this.$el.addClass(this.model.get("device"));

    this._setupText();

    var self = this;

    setTimeout(function() {
      self._applyStyle();
      self._place();
      self.show();
    }, 500);

    cdb.god.unbind("closeDialogs", this._onCloseDialogs, this);
    cdb.god.bind("closeDialogs", this._onCloseDialogs, this);

    return this;

  }

});