BrettBukowski/cropper

View on GitHub
lib/CropPane.js

Summary

Maintainability
A
2 hrs
Test Coverage
"use strict";

var Resizer = require('./Resizer'),
    Position = require('./Position'),
    domTool = require('./DOMTools'),
    draggable = require('./Draggable'),
    emitter = require('emitter'),
    bind = require('bind');

module.exports = CropPane;

/**
 * # CropPane
 * @param {Object} options Options hash
 */
function CropPane (options) {
  this.options = options;

  this.setRatio(this.options.ratio);

  emitter(this);
  draggable(this);
}

CropPane.prototype = {
  /**
   * Draws the pane for the first time.
   * @param  {Object} reference Parent element
   */
  draw: function (reference) {
    this.el || (this._createPane(reference));
  },

  /**
   * Moves to the given coordinates.
   * @param  {Number=} x
   * @param  {Number=} y
   */
  moveTo: function (x, y) {
    var movement = {};

    if (typeof x === 'number') {
      movement.left = x + 'px';
    }
    if (typeof y === 'number') {
      movement.top = y + 'px';
    }

    this.refresh(movement);
  },

  /**
   * Sets the ratio.
   * @param {String} ratio none|square|number:number
   */
  setRatio: function (ratio) {
    ratio = ratio.toString().toLowerCase();

    if (ratio === 'none' || ratio === 'square') {
      this.ratio = ratio;
    }
    else if (/^\d{1,2}:\d{1,2}$/.test(ratio)) {
      var split = ratio.split(':');
      this.ratio = parseInt(split[0], 10) / parseInt(split[1], 10);
    }
    else {
      throw new TypeError("Don't know what to do with " + ratio);
    }
  },

  /**
   * Hook method for Draggable interface.
   * @param  {Number} diffX Difference in x coordinates
   * @param  {Number} diffY Difference in y coordinates
   */
  onDrag: function (diffX, diffY) {
    var newX, newY;

    if (diffX) {
      newX = parseFloat(this.el.style.left) + diffX;
    }
    if (diffY) {
      newY = parseFloat(this.el.style.top) + diffY;
    }

    this.moveTo(newX, newY);
  },

  /**
   * Redraws with the given transforms.
   * @param  {Object=} changes CSS styles to change (if any)
   */
  refresh: function (changes) {
    this._performTransform(changes);
    // **Change Event**
    this.emit('change', Position.relative(this.el));
  },

  destroy: function () {
    domTool.remove(this.parentBounds);
    domTool.remove(this.el);

    this.parentBounds = this.el = null;
  },

  _keepInBounds: function (coordinate, max, min) {
    coordinate = parseFloat(coordinate);
    coordinate = Math.max(coordinate, min || 0);
    coordinate = Math.min(coordinate, max || Number.MAX_VALUE);

    return coordinate;
  },

  /**
   * Modifies the height attribute to properly conform to
   * the current crop ratio.
   * @param  {Object=} pendingTransforms Transforms to be applied (if any)
   * @return {Object=} Object with modified height property or pendingTransforms
   */
  _enforceRatio: function (pendingTransforms) {
    var currentWidth = (pendingTransforms) ?
            parseFloat(pendingTransforms.width) :
            Position.relative(this.el).width,
        newHeight,
        ratio = this.ratio;

    if (ratio === 'square') {
      newHeight = currentWidth;
    }
    else if (typeof ratio === 'number') {
      newHeight = currentWidth / ratio;
    }
    if (newHeight) {
      pendingTransforms || (pendingTransforms = {});
      pendingTransforms.height = newHeight + 'px';
    }

    return pendingTransforms;
  },

  _enforceBounds: function (pendingTransforms) {
    var currentSize = Position.relative(this.el),
        bounds = {
          left:   { parent: 'width', self: 'width' },
          width:  { parent: 'width', self: 'x' },
          top:    { parent: 'height', self: 'height' },
          height: { parent: 'height', self: 'y' }
        },
        bound,
        boundCalculation;

    for (bound in bounds) {
      if (bounds.hasOwnProperty(bound) && bound in pendingTransforms) {
        boundCalculation = bounds[bound];
        pendingTransforms[bound] = this._keepInBounds(pendingTransforms[bound], domTool[boundCalculation.parent](this.parentBounds) - currentSize[boundCalculation.self]) + 'px';
      }
    }

    return pendingTransforms;
  },

  _enforceSize: function (pendingTransforms) {
    if ('width' in pendingTransforms) {
      pendingTransforms.width = this._keepInBounds(pendingTransforms.width, this.options.maxWidth, this.options.minWidth) + 'px';
    }
    if ('height' in pendingTransforms) {
      pendingTransforms.height = this._keepInBounds(pendingTransforms.height, this.options.maxHeight, this.options.minHeight) + 'px';
    }

    return pendingTransforms;
  },

  _performTransform: function (changes) {
    changes = this._enforceRatio(changes);

    if (changes) {
      changes = this._enforceBounds(changes);
      changes = this._enforceSize(changes);

      domTool.setStyles(this.el, changes);
    }
  },

  _createPane: function (reference) {
    this.parentBounds = this._createBounds(reference);
    var parentSize = Position.relative(this.parentBounds);

    var el = document.createElement('div');
    el.className = 'cropper-crop-pane';

    this.el = this.parentBounds.appendChild(el);
    this._performTransform({
      position: 'absolute',
      height:   this.options.defaultHeight + 'px',
      width:    this.options.defaultWidth + 'px',
      top:      (parentSize.height / 2) - (this.options.defaultHeight / 2) + 'px',
      left:     (parentSize.width / 2) - (this.options.defaultWidth / 2) + 'px'
    });

    this.makeDraggable(el, this.parentBounds);
    this._addResizers(el);

    this.refresh();
  },

  _addResizers: function (el) {
    for (var i = 0, regions = ['ne', 'se', 'nw', 'sw'], len = regions.length, sizer; i < len; i++) {
      sizer = new Resizer(regions[i], this.parentBounds);
      sizer.on('move', bind(this, this._onResize));
      el.appendChild(sizer.el);
    }
  },

  /**
   * Handler for resizer's 'move' event.
   * @param  {String} region ne, se, nw, sw
   * @param  {Number} x      Delta x
   * @param  {Number} y      Delta y
   */
  _onResize: function (region, x, y) {
    var currentPosition = Position.relative(this.el),
        operations = this._bounds[region],
        transform = {};

    for (var bound in operations) {
      if (operations.hasOwnProperty(bound)) {
        transform[bound] = this._bounds[bound](currentPosition, x, y, operations[bound]);
      }
    }

    this.refresh(transform);
  },

  _bounds: {
    ne: {
      width:  1,
      top:    1,
      height: -1
    },
    se: {
      height: 1,
      width:  1
    },
    nw: {
      top:    1,
      height: -1,
      left:   1,
      width:  -1
    },
    sw: {
      height: 1,
      left:   1,
      width:  -1
    },

    width: function (currentPosition, x, y, direction) {
      return currentPosition.width + (x * direction) + 'px';
    },
    height: function (currentPosition, x, y, direction) {
      return currentPosition.height + (y * direction) + 'px';
    },
    top: function (currentPosition, x, y, direction) {
      return currentPosition.y + (y * direction) + 'px';
    },
    left: function (currentPosition, x, y, direction) {
      return currentPosition.x + (x * direction) + 'px';
    }
  },

  _createBounds: function (reference) {
    var el = document.createElement('div');

    domTool.setStyles(el, {
      position: 'absolute',
      zIndex:   101
    });
    domTool.keepSnapped(el, reference);

    return reference.parentNode.insertBefore(el, reference);
  }
};