juanmard/icestudio

View on GitHub
graphics/joint.selection.js

Summary

Maintainability
D
2 days
Test Coverage
/*
Copyright (c) 2016-2019 FPGAwars
Copyright (c) 2013 client IO
*/

/* eslint-disable new-cap */

joint.ui.SelectionView = Backbone.View.extend({
  className: 'selection',

  events: {
    'click .selection-box': 'click',
    dblclick: 'dblclick',
    'mousedown .selection-box': 'startTranslatingSelection',
    mouseover: 'mouseover',
    mouseout: 'mouseout',
    mouseup: 'mouseup',
    mousedown: 'mousedown',
  },

  showtooltip: true,
  $selectionArea: null,

  initialize: function (options) {
    'use strict';

    _.bindAll(
      this,
      'click',
      'startSelecting',
      'stopSelecting',
      'adjustSelection'
    );

    $(document.body).on(
      'mouseup touchend',
      function (evt) {
        if (evt.which === 1) {
          // Mouse left button
          this.stopSelecting(evt);
        }
      }.bind(this)
    );
    $(document.body).on('mousemove touchmove', this.adjustSelection);

    this.options = options;

    this.options.paper.$el.append(this.$el);
    this.$el.addClass('selected').show();
  },

  click: function (evt) {
    'use strict';

    if (evt.which === 1) {
      // Mouse left button
      this.trigger('selection-box:pointerclick', evt);
    }
  },

  dblclick: function (evt) {
    'use strict';

    var id = evt.target.getAttribute('data-model');
    if (id) {
      var view = this.options.paper.findViewByModel(id);
      if (view) {
        // Trigger dblclick in selection to the Cell View
        view.notify('cell:pointerdblclick', evt);
      }
    }
  },

  mouseover: function (evt) {
    'use strict';

    this.mouseManager(evt, 'mouseovercard');
  },

  mouseout: function (evt) {
    'use strict';

    this.mouseManager(evt, 'mouseoutcard');
  },

  mouseup: function (evt) {
    'use strict';

    this.mouseManager(evt, 'mouseupcard');
  },

  mousedown: function (evt) {
    'use strict';

    if (!this.showtooltip && evt.which === 1) {
      // Mouse left button: block fixed
      this.showtooltip = true;
    }

    this.mouseManager(evt, 'mousedowncard');
  },

  mouseManager: function (evt, fnc) {
    'use strict';

    evt.preventDefault();

    if (this.showtooltip) {
      var id = evt.target.getAttribute('data-model');
      if (id) {
        var view = this.options.paper.findViewByModel(id);
        if (view && view[fnc]) {
          view[fnc].apply(view, [evt]);
        }
      }
    }
  },

  startTranslatingSelection: function (evt) {
    'use strict';

    if (this._action !== 'adding' && evt.which === 1) {
      // Mouse left button

      if (!evt.shiftKey) {
        this._action = 'translating';

        this.options.graph.trigger('batch:stop');
        this.options.graph.trigger('batch:start');

        var snappedClientCoords = this.options.paper.snapToGrid(
          g.point(evt.clientX, evt.clientY)
        );
        this._snappedClientX = snappedClientCoords.x;
        this._snappedClientY = snappedClientCoords.y;

        this.trigger('selection-box:pointerdown', evt);
      }
    }
  },

  startAddingSelection: function (evt) {
    'use strict';
    this._action = 'adding';

    var snappedClientCoords = this.options.paper.snapToGrid(
      g.point(evt.clientX, evt.clientY)
    );
    this._snappedClientX = snappedClientCoords.x;
    this._snappedClientY = snappedClientCoords.y;

    this.trigger('selection-box:pointerdown', evt);
  },

  startSelecting: function (evt /*, x, y*/) {
    'use strict';

    this.createSelectionArea();

    this._action = 'selecting';

    this._clientX = evt.clientX;
    this._clientY = evt.clientY;

    // Normalize `evt.offsetX`/`evt.offsetY` for browsers that don't support it (Firefox).
    var paperElement = evt.target.parentElement || evt.target.parentNode;
    var paperOffset = $(paperElement).offset();
    var paperScrollLeft = paperElement.scrollLeft;
    var paperScrollTop = paperElement.scrollTop;

    this._offsetX =
      evt.offsetX === undefined
        ? evt.clientX - paperOffset.left + window.pageXOffset + paperScrollLeft
        : evt.offsetX;
    this._offsetY =
      evt.offsetY === undefined
        ? evt.clientY - paperOffset.top + window.pageYOffset + paperScrollTop
        : evt.offsetY;

    this.$selectionArea.css({
      width: 1,
      height: 1,
      left: this._offsetX,
      top: this._offsetY,
    });
  },

  adjustSelection: function (evt) {
    'use strict';

    var dx;
    var dy;

    switch (this._action) {
      case 'selecting':
        dx = evt.clientX - this._clientX;
        dy = evt.clientY - this._clientY;

        var left = parseInt(this.$selectionArea.css('left'), 10);
        var top = parseInt(this.$selectionArea.css('top'), 10);

        this.$selectionArea.css({
          left: dx < 0 ? this._offsetX + dx : left,
          top: dy < 0 ? this._offsetY + dy : top,
          width: Math.abs(dx),
          height: Math.abs(dy),
        });
        break;

      case 'adding':
      case 'translating':
        var snappedClientCoords = this.options.paper.snapToGrid(
          g.point(evt.clientX, evt.clientY)
        );
        var snappedClientX = snappedClientCoords.x;
        var snappedClientY = snappedClientCoords.y;

        dx = snappedClientX - this._snappedClientX;
        dy = snappedClientY - this._snappedClientY;

        // This hash of flags makes sure we're not adjusting vertices of one link twice.
        // This could happen as one link can be an inbound link of one element in the selection
        // and outbound link of another at the same time.
        var processedLinks = {};

        this.model.each(function (element) {
          // Translate the element itself.
          element.translate(dx, dy);

          // Translate also the `selection-box` of the element.
          this.updateBox(element);

          // Translate link vertices as well.
          var connectedLinks = this.options.graph.getConnectedLinks(element);

          _.each(connectedLinks, function (link) {
            if (processedLinks[link.id]) {
              return;
            }

            var vertices = link.get('vertices');
            if (vertices && vertices.length) {
              var newVertices = [];
              _.each(vertices, function (vertex) {
                newVertices.push({x: vertex.x + dx, y: vertex.y + dy});
              });

              link.set('vertices', newVertices);
            }

            processedLinks[link.id] = true;
          });
        }, this);

        if (dx || dy) {
          this._snappedClientX = snappedClientX;
          this._snappedClientY = snappedClientY;
        }

        this.trigger('selection-box:pointermove', evt);

        break;
    }
  },

  stopSelecting: function (evt) {
    'use strict';

    switch (this._action) {
      case 'selecting':
        if (!evt.shiftKey) {
          // Reset previous selection
          this.cancelSelection();
        }

        var offset = this.$selectionArea.offset();
        var width = this.$selectionArea.width();
        var height = this.$selectionArea.height();

        // Convert offset coordinates to the local point of the <svg> root element.
        var localPoint = V(this.options.paper.svg).toLocalPoint(
          offset.left,
          offset.top
        );

        // Take page scroll into consideration.
        localPoint.x -= window.pageXOffset;
        localPoint.y -= window.pageYOffset;

        var elementViews = this.findBlocksInArea(
          g.rect(localPoint.x, localPoint.y, width, height),
          {strict: false}
        );

        this.model.add(_.pluck(elementViews, 'model'));

        _.each(this.model.models, this.createSelectionBox, this);

        this.destroySelectionArea();

        break;

      case 'translating':
        this.options.graph.trigger('batch:stop');
        // Everything else is done during the translation.
        break;

      case 'adding':
        break;

      case 'cherry-picking':
        // noop;  All is done in the `createSelectionBox()` function.
        // This is here to avoid removing selection boxes as a reaction on mouseup event and
        // propagating to the `default` branch in this switch.
        break;

      default:
        break;
    }

    delete this._action;
  },

  findBlocksInArea: function (rect, opt) {
    'use strict';

    opt = _.defaults(opt || {}, {strict: false});
    rect = g.rect(rect);

    var paper = this.options.paper;
    var views = _.map(paper.model.getElements(), paper.findViewByModel, paper);
    var method = opt.strict ? 'containsRect' : 'intersect';

    return _.filter(
      views,
      function (view) {
        var $box = $(view.$box[0]);
        var position = $box.position();
        var rbox = g.rect(
          position.left,
          position.top,
          $box.width(),
          $box.height()
        );
        return view && rect[method](rbox);
      },
      this
    );
  },

  cancelSelection: function () {
    'use strict';

    this.$('.selection-box').remove();
    this.model.reset([]);
  },

  destroySelectionArea: function () {
    'use strict';

    this.$selectionArea.remove();
    this.$selectionArea = this.$('.selection-area');
    this.$el.addClass('selected');
  },

  createSelectionArea: function () {
    'use strict';

    var $selectionArea = $('<div/>', {
      class: 'selection-area',
    });
    this.$el.append($selectionArea);
    this.$selectionArea = this.$('.selection-area');
    this.$el.removeClass('selected');
  },

  destroySelectionBox: function (element) {
    'use strict';

    this.$('[data-model="' + element.get('id') + '"]').remove();
  },

  createSelectionBox: function (element, opt) {
    'use strict';

    opt = opt || {};

    if (!element.isLink()) {
      var $selectionBox = $('<div/>', {
        class: 'selection-box',
        'data-model': element.get('id'),
      });
      if (this.$('[data-model="' + element.get('id') + '"]').length === 0) {
        this.$el.append($selectionBox);
      }
      this.showtooltip = opt.initooltip !== undefined ? opt.initooltip : true;
      $selectionBox.css({opacity: opt.transparent ? 0 : 1});

      this.updateBox(element);

      this._action = 'cherry-picking';
    }
  },

  updateBox: function (element) {
    'use strict';

    var bbox = element.getBBox();
    var state = this.options.state;

    var i,
      pendingTasks = [];
    var sels = document.querySelectorAll(
      'div[data-model="' + element.get('id') + '"]'
    );
    for (i = 0; i < sels.length; i++) {
      pendingTasks.push({
        e: sels[i],
        property: 'left',
        value:
          Math.round(
            bbox.x * state.zoom +
              state.pan.x +
              (bbox.width / 2.0) * (state.zoom - 1)
          ) + 'px',
      });
      pendingTasks.push({
        e: sels[i],
        property: 'top',
        value:
          Math.round(
            bbox.y * state.zoom +
              state.pan.y +
              (bbox.height / 2.0) * (state.zoom - 1)
          ) + 'px',
      });
      pendingTasks.push({
        e: sels[i],
        property: 'width',
        value: Math.round(bbox.width) + 'px',
      });
      pendingTasks.push({
        e: sels[i],
        property: 'height',
        value: Math.round(bbox.height) + 'px',
      });
      pendingTasks.push({
        e: sels[i],
        property: 'transform',
        value: 'scale(' + state.zoom + ')',
      });
    }
    i = pendingTasks.length;
    for (i = 0; i < pendingTasks.length; i++) {
      if (pendingTasks[i].e !== null) {
        pendingTasks[i].e.style[pendingTasks[i].property] =
          pendingTasks[i].value;
      }
    }
  },
});