fabric.Object.prototype.offsetCorner = 12;

//Start: Textbox with a max width
fabric.util.object.extend(fabric.Textbox.prototype, {
maxWidth: 0,
fixedWidth: true,
hyphenation: false,
get2DCursorLocation: function get2DCursorLocation(selectionStart, skipWrapping) {
  if (typeof selectionStart === "undefined") {
    selectionStart = this.selectionStart;
  }
  var lines = skipWrapping ? this._unwrappedTextLines : this._textLines;
  var len = lines.length;
  for (var i = 0; i < len; i++) {
    if (selectionStart <= lines[i].length) {
      return {
        lineIndex: i,
        charIndex: selectionStart
      };
    }
    if (this._longLines[i]) {
      selectionStart++;
    }
    selectionStart -= lines[i].length + 1;
  }
  return {
    lineIndex: i - 1,
    charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart
  };
},
_renderText: function _renderText(ctx) {
  if (this.paintFirst === "stroke") {
    this._renderTextStroke(ctx);
    this._renderTextFill(ctx);
  } else {
    this._renderTextFill(ctx);
    this._renderTextStroke(ctx);
  }
  if (this.hyphenation) {
    this._renderTextOversize(ctx);
  }
},
_renderTextOversize: function _renderTextOversize(ctx) {
  var lineHeight = 0;
  for (var i = 0, len = this._textLines.length; i < len; i++) {
    var lineWidth = this.measureLine(i).width;
    var lineLeftOffset = this._getLineLeftOffset(i);
    var heightOfLine = this.getHeightOfLine(i);
    if (this._longLines[i]) {
      ctx.fillRect(this._getLeftOffset() + lineLeftOffset + lineWidth + 2, this._getTopOffset() + lineHeight + heightOfLine / 2 - 1, 5, this.fontSize / 15);
    }
    lineHeight += heightOfLine;
  }
},
_getNewSelectionStartFromOffset: function _getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, index, jlen) {
  var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,
    distanceBtwNextCharAndCursor = width - mouseOffset.x,
    offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor || distanceBtwNextCharAndCursor < 0 ? 0 : 1,
    newSelectionStart = index + offset;

  if (this.flipX) {
    newSelectionStart = jlen - newSelectionStart;
  }
  // the index passed into the function is padded by the amount of lines from _textLines (to account for \n)
  // we need to remove this padding, and pad it by actual lines, and / or spaces that are meant to be there
  var tmp = 0,
    removed = 0,
    _long = 0; //modified @den.ponomarev

  // account for removed characters
  for (var i = 0; i < this._textLines.length; i++) {
    tmp += this._textLines[i].length;
    if (tmp + removed >= newSelectionStart) {
      break;
    }
    //modified @den.ponomarev
    if (this._longLines[i]) {
      newSelectionStart--;
      _long++;
    }
    if (this.text[tmp + removed] === '\n' || this.text[tmp + removed] === ' ') {
      removed++;
    }
  }
  if (newSelectionStart > this.text.length) {
    newSelectionStart = this.text.length;
  }
  //modified @den.ponomarev
  return newSelectionStart - i + removed + _long;
  //return newSelectionStart + _long;
},
_wrapLine: function _wrapLine(_line, lineIndex, desiredWidth) {
  var lineWidth = 0,
    graphemeLines = [],
    line = [],
    words = _line.split(this._reSpaceAndTab),
    word = "",
    offset = 0,
    infix = " ",
    wordWidth = 0,
    infixWidth = 0,
    largestWordWidth = 0,
    lineJustStarted = true,
    additionalSpace = this._getWidthOfCharSpacing();

  this._longLines = [];
  //todo desiredWidth
  var _maxWidth = this.maxWidth || this.fixedWidth && this.width;
  var isLongWord = false;

  for (var i = 0; i < words.length; i++) {
    word = fabric.util.string.graphemeSplit(words[i]);
    wordWidth = this._measureWord(word, lineIndex, offset);

    if (!this.breakWords) {
      var _isLong = _maxWidth && wordWidth > _maxWidth;

      if (_isLong) {
        if (line.length != 0) {
          graphemeLines.push(line);
          this._longLines.push(isLongWord);
          isLongWord = false;
          lineWidth = 0;
          line = [];
        }

        var _hypheSize = 0;
        var _bigWordWidth = 0; // lineWidth + infixWidth;
        for (var k = 0, len = word.length; k < len && _bigWordWidth <= _maxWidth - _hypheSize; k++) {
          _bigWordWidth += this._measureWord(word[k], lineIndex, k + offset);
        }
        var new_word = word.splice(0, k - 1);
        isLongWord = true;
        words.splice(i, 1, new_word.join(""), word.join(""));
        i--;
        lineJustStarted = true;
        continue;
      }
    }
    lineWidth += infixWidth + wordWidth - additionalSpace;

    if (lineWidth >= this.width) {

      if (this.breakWords) {
        lineWidth -= wordWidth;
        line.push(infix);
        var wordLetters = word.splice(0);

        while (wordLetters.length) {
          var letterWidth = this._measureWord(wordLetters[0], lineIndex, offset);
          if (lineWidth + letterWidth > this.width) {
            graphemeLines.push(line);
            this._longLines.push(true);
            line = [];
            lineWidth = 0;
          }
          line.push(wordLetters.shift());
          offset++;
          lineWidth += letterWidth;
        }
      } else if (!lineJustStarted) {
        graphemeLines.push(line);
        this._longLines.push(isLongWord);
        isLongWord = false;
        line = [];
        lineWidth = wordWidth;
        lineJustStarted = true;
      }
    } else {
      lineWidth += additionalSpace;
    }
    offset += word.length;

    if (!lineJustStarted) {
      line.push(infix);
    }
    line = line.concat(word);

    infixWidth = this._measureWord(infix, lineIndex, offset);
    offset++;

    // keep track of largest word
    if (wordWidth > largestWordWidth) {
      largestWordWidth = wordWidth;
    }
    lineJustStarted = false;
  }

  i && graphemeLines.push(line);
  this._longLines.push(false);

  if (this.breakWords) {
    this.dynamicMinWidth = 0;
  } else if (largestWordWidth > this.dynamicMinWidth) {
    this.dynamicMinWidth = largestWordWidth - additionalSpace;
  }
  return graphemeLines;
}
});
//End: Textbox with a max width


//Start: Snapping
fabric.util.snapToGrid = function (target, grid, tolerance) {
  if(typeof Symbol === 'undefined') {return;}
var result = null;
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

try {
  for (var _iterator = target.x[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
    var targetX = _step.value;

    var dX = (targetX + grid.offsetX - tolerance) % grid.size - tolerance / 2;
    var distance = Math.abs(dX);
    if (distance < tolerance) {
      if (!result) result = {};
      result.x = targetX + distance;
      result.tx = targetX;
      result.dx = dX;
    }
  }
} catch (err) {
  _didIteratorError = true;
  _iteratorError = err;
} finally {
  try {
    if (!_iteratorNormalCompletion && _iterator.return) {
      _iterator.return();
    }
  } finally {
    if (_didIteratorError) {
      throw _iteratorError;
    }
  }
}

var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;

try {
  for (var _iterator2 = target.y[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
    var targetY = _step2.value;

    var dY = (targetY + grid.offsetY - tolerance) % grid.size - tolerance / 2;
    var _distance = Math.abs(dY);
    if (_distance < tolerance) {
      if (!result) result = {};
      result.y = targetY + _distance;
      result.ty = targetY;
      result.dy = dY;
    }
  }
} catch (err) {
  _didIteratorError2 = true;
  _iteratorError2 = err;
} finally {
  try {
    if (!_iteratorNormalCompletion2 && _iterator2.return) {
      _iterator2.return();
    }
  } finally {
    if (_didIteratorError2) {
      throw _iteratorError2;
    }
  }
}

if (result) {
  result.distance = Math.abs(Math.round(result.dx && result.dy && Math.min(Math.abs(result.dx), Math.abs(result.dy)) || result.dx || result.dy));
}
return result;
};

/**
* Returns Snapping to Objects Control Points Point
* @param {SnappingObject} target
* @param {Array<SnappingObject>} objects
* @param {number} tolerance
* @returns {SnapToResult | null}
*/
fabric.util.snapToPoints = function (target, objects, tolerance) {
  if(typeof Symbol === 'undefined') {return;}
var result = null;
var minDistance = tolerance;
//supportlines
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;

try {
  for (var _iterator3 = objects[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
    var object = _step3.value;

    if (object.points) {
      var _iteratorNormalCompletion4 = true;
      var _didIteratorError4 = false;
      var _iteratorError4 = undefined;

      try {
        for (var _iterator4 = object.points[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
          var point = _step4.value;
          var _iteratorNormalCompletion5 = true;
          var _didIteratorError5 = false;
          var _iteratorError5 = undefined;

          try {
            for (var _iterator5 = target.points[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
              var targetPoint = _step5.value;

              var dX = point.x - targetPoint.x;
              var dY = point.y - targetPoint.y;
              var distance = (Math.abs(dX) + Math.abs(dY)) / 2;
              if (distance < minDistance) {
                minDistance = distance;
                result = {
                  object: object,
                  x: point.x, tx: targetPoint.x, dx: dX,
                  y: point.y, ty: targetPoint.y, dy: dY
                };
              }
            }
          } catch (err) {
            _didIteratorError5 = true;
            _iteratorError5 = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion5 && _iterator5.return) {
                _iterator5.return();
              }
            } finally {
              if (_didIteratorError5) {
                throw _iteratorError5;
              }
            }
          }
        }
      } catch (err) {
        _didIteratorError4 = true;
        _iteratorError4 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion4 && _iterator4.return) {
            _iterator4.return();
          }
        } finally {
          if (_didIteratorError4) {
            throw _iteratorError4;
          }
        }
      }
    }
  }
} catch (err) {
  _didIteratorError3 = true;
  _iteratorError3 = err;
} finally {
  try {
    if (!_iteratorNormalCompletion3 && _iterator3.return) {
      _iterator3.return();
    }
  } finally {
    if (_didIteratorError3) {
      throw _iteratorError3;
    }
  }
}

if (result) {
  result.distance = Math.round(Math.min(Math.abs(result.dx), Math.abs(result.dy)));
}
return result;
};

/**
* Returns Snapping to Objects Bounding Rectangles Point
* @param {SnappingObject} target
* @param {Array<SnappingObject>} objects
* @param {number} tolerance
* @returns {SnapToResult | null}
*/
fabric.util.snapToBounds = function (target, objects, tolerance) {
  if(typeof Symbol === 'undefined') {return;}
var result = null;
var minDistance = tolerance;

//snap center point to vertical lines
if (target.cx) {
  var _iteratorNormalCompletion6 = true;
  var _didIteratorError6 = false;
  var _iteratorError6 = undefined;

  try {
    for (var _iterator6 = objects[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
      var object = _step6.value;

      if (object.cx && object.cy === undefined) {
        var dX = object.cx - target.cx;
        var distance = Math.abs(dX);
        if (distance < minDistance) {
          minDistance = distance;
          if (!result) result = {};
          result.objectX = object;
          result.x = object.cx;
          result.tx = target.cx;
          result.dx = dX;
        }
      }
    }
  } catch (err) {
    _didIteratorError6 = true;
    _iteratorError6 = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion6 && _iterator6.return) {
        _iterator6.return();
      }
    } finally {
      if (_didIteratorError6) {
        throw _iteratorError6;
      }
    }
  }
}
//snap center point to horisontal lines
minDistance = tolerance;
if (target.cy) {
  var _iteratorNormalCompletion7 = true;
  var _didIteratorError7 = false;
  var _iteratorError7 = undefined;

  try {
    for (var _iterator7 = objects[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
      var _object = _step7.value;

      if (_object.cy && _object.cx === undefined) {
        var dy = _object.cy - target.cy;
        var _distance2 = Math.abs(dy);
        if (_distance2 < minDistance) {
          minDistance = _distance2;
          if (!result) result = {};
          result.objectY = _object;
          result.y = _object.cy;
          result.ty = target.cy;
          result.dy = dy;
        }
      }
    }
  } catch (err) {
    _didIteratorError7 = true;
    _iteratorError7 = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion7 && _iterator7.return) {
        _iterator7.return();
      }
    } finally {
      if (_didIteratorError7) {
        throw _iteratorError7;
      }
    }
  }
}
//snap edges to horisontal lines
minDistance = tolerance;
var _iteratorNormalCompletion8 = true;
var _didIteratorError8 = false;
var _iteratorError8 = undefined;

try {
  for (var _iterator8 = objects[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
    var _object2 = _step8.value;

    if (_object2.x) {
      var _iteratorNormalCompletion10 = true;
      var _didIteratorError10 = false;
      var _iteratorError10 = undefined;

      try {
        for (var _iterator10 = (_object2.x.length ? _object2.x : [_object2.x])[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) {
          var x = _step10.value;
          var _iteratorNormalCompletion11 = true;
          var _didIteratorError11 = false;
          var _iteratorError11 = undefined;

          try {
            for (var _iterator11 = target.x[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) {
              var targetX = _step11.value;

              var _dX = x - targetX;
              var _distance3 = Math.abs(_dX);
              if (_distance3 < minDistance) {
                minDistance = _distance3;
                if (!result) result = {};
                result.objectX = _object2;
                result.x = x;
                result.tx = targetX;
                result.dx = _dX;
              }
            }
          } catch (err) {
            _didIteratorError11 = true;
            _iteratorError11 = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion11 && _iterator11.return) {
                _iterator11.return();
              }
            } finally {
              if (_didIteratorError11) {
                throw _iteratorError11;
              }
            }
          }
        }
      } catch (err) {
        _didIteratorError10 = true;
        _iteratorError10 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion10 && _iterator10.return) {
            _iterator10.return();
          }
        } finally {
          if (_didIteratorError10) {
            throw _iteratorError10;
          }
        }
      }
    }
  }
} catch (err) {
  _didIteratorError8 = true;
  _iteratorError8 = err;
} finally {
  try {
    if (!_iteratorNormalCompletion8 && _iterator8.return) {
      _iterator8.return();
    }
  } finally {
    if (_didIteratorError8) {
      throw _iteratorError8;
    }
  }
}

minDistance = tolerance;
//snap edges to vertical lines
var _iteratorNormalCompletion9 = true;
var _didIteratorError9 = false;
var _iteratorError9 = undefined;

try {
  for (var _iterator9 = objects[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) {
    var _object3 = _step9.value;

    if (_object3.y) {
      var _iteratorNormalCompletion12 = true;
      var _didIteratorError12 = false;
      var _iteratorError12 = undefined;

      try {
        for (var _iterator12 = (_object3.y.length ? _object3.y : [_object3.y])[Symbol.iterator](), _step12; !(_iteratorNormalCompletion12 = (_step12 = _iterator12.next()).done); _iteratorNormalCompletion12 = true) {
          var y = _step12.value;
          var _iteratorNormalCompletion13 = true;
          var _didIteratorError13 = false;
          var _iteratorError13 = undefined;

          try {
            for (var _iterator13 = target.y[Symbol.iterator](), _step13; !(_iteratorNormalCompletion13 = (_step13 = _iterator13.next()).done); _iteratorNormalCompletion13 = true) {
              var targetY = _step13.value;

              var dY = y - targetY;
              var _distance4 = Math.abs(dY);
              if (_distance4 < minDistance) {
                minDistance = _distance4;
                if (!result) result = {};
                result.objectY = _object3;
                result.y = y;
                result.ty = targetY;
                result.dy = dY;
              }
            }
          } catch (err) {
            _didIteratorError13 = true;
            _iteratorError13 = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion13 && _iterator13.return) {
                _iterator13.return();
              }
            } finally {
              if (_didIteratorError13) {
                throw _iteratorError13;
              }
            }
          }
        }
      } catch (err) {
        _didIteratorError12 = true;
        _iteratorError12 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion12 && _iterator12.return) {
            _iterator12.return();
          }
        } finally {
          if (_didIteratorError12) {
            throw _iteratorError12;
          }
        }
      }
    }
  }
} catch (err) {
  _didIteratorError9 = true;
  _iteratorError9 = err;
} finally {
  try {
    if (!_iteratorNormalCompletion9 && _iterator9.return) {
      _iterator9.return();
    }
  } finally {
    if (_didIteratorError9) {
      throw _iteratorError9;
    }
  }
}

if (result) {
  result.distance = Math.abs(Math.round(result.dx && result.dy && Math.min(Math.abs(result.dx), Math.abs(result.dy)) || result.dx || result.dy));
}
return result;
};

/**
* generate SnappingObject using in snapping cache
* @param {Array<fabric.Object>} object - FabricJS object
* @returns {SnappingObject}
*/
fabric.util.createSnapObject = function (object) {
var cr = object.calcCoords(true),
  xPoints = [cr.tl.x, cr.tr.x, cr.bl.x, cr.br.x],
  yPoints = [cr.tl.y, cr.tr.y, cr.bl.y, cr.br.y];

var xMin = fabric.util.array.min(xPoints),
  xMax = fabric.util.array.max(xPoints),
  yMin = fabric.util.array.min(yPoints),
  yMax = fabric.util.array.max(yPoints);
return {
  instance: object,
  points: [cr.tl, cr.tr, cr.br, cr.bl],
  cx: xMin + (xMax - xMin) / 2,
  cy: yMin + (yMax - yMin) / 2,
  x: [xMin, xMax],
  y: [yMin, yMax]
};
};

/**
* Draw Helper lines For Snapping function
* @param {SnapToResult} snapResult
* @param strokeStyle
*/
fabric.Canvas.prototype.renderSnapping = function (snapResult, strokeStyle) {
var ctx = this.contextTop,
  v = this.viewportTransform;
if (this._currentTransform && snapResult) {
  this.clearContext(ctx);
}
ctx.save();
ctx.strokeStyle = strokeStyle;
ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);

function drawSnapObject(obj) {
  if (!obj || !obj.points) return;
  var p = obj.points;
  ctx.moveTo(p[0].x, p[0].y);
  for (var i = p.length; i--;) {
    ctx.lineTo(p[i].x, p[i].y);
  }
}

ctx.beginPath();
drawSnapObject(snapResult.object);
drawSnapObject(snapResult.objectX);
drawSnapObject(snapResult.objectY);
drawSnapObject(this.__snapCache.target);
if (snapResult.object) {
  ctx.moveTo(snapResult.tx, snapResult.ty);
  ctx.lineTo(snapResult.x, snapResult.y);
  ctx.arc(snapResult.x, snapResult.y, 2, 0, 2 * Math.PI);
}
if (snapResult.objectX) {
  ctx.moveTo(snapResult.x, 0);
  ctx.lineTo(snapResult.x, this.originalHeight);
}
if (snapResult.objectY) {
  ctx.moveTo(0, snapResult.y);
  ctx.lineTo(this.originalWidth, snapResult.y);
}
ctx.stroke();
ctx.restore();
};

/**
* Correct Object moving on "object:moving" event
* @param {Object}                options           -
* @param {fabric.Object}         options.target    - moving object
* @param {Array<SnappingObject>} options.guidlines - other guidlines
* @param {Array<fabric.Object>}  options.objects   - other objects
* @param {GridOptions}           options.grid      -
* @param {number}                options.tolerance -
* @returns {SnapToResult | null}
*/
fabric.Canvas.prototype.gridSnapMove = function (options) {
var area = options.area;
var objects = options.objects || this._objects;
if (!this.__snapCache) {
  var snapObjects = options.guidlines || [];
  for (var i in objects) {
    var obj = objects[i];
    if (obj === options.target || !obj.snappable || !obj.visible) continue;
    snapObjects.push(fabric.util.createSnapObject(obj));
  }

  if (area) {
    snapObjects.push({
      instance: this,
      x: [area.x1, area.x2],
      y: [area.y1, area.y2]
    });
  }

  this.__snapCache = {
    area: area,
    objects: snapObjects,
    grid: options.grid
  };
}

this.__snapCache.target = fabric.util.createSnapObject(options.target);

var snapTo = null;
var snapPoints = fabric.util.snapToPoints(this.__snapCache.target, this.__snapCache.objects, options.tolerance);
if (!snapTo) snapTo = snapPoints;
if (snapTo && snapPoints && snapPoints.distance < snapTo.distance) {
  snapTo = snapPoints;
}

var snapRects = fabric.util.snapToBounds(this.__snapCache.target, this.__snapCache.objects, options.tolerance);
if (!snapTo) snapTo = snapRects;
if (snapTo && snapRects && snapRects.distance < snapTo.distance) {
  snapTo = snapRects;
}

if (options.grid) {
  var snapGrid = fabric.util.snapToGrid(this.__snapCache.target, this.__snapCache.grid, options.tolerance);
  if (!snapTo) snapTo = snapGrid;
  if (snapTo && snapGrid && snapGrid.distance < snapTo.distance) {
    snapTo = snapGrid;
  }
}

if (snapTo) {
  if (snapTo.dx) {
    options.target.left += snapTo.dx;
  }
  if (snapTo.dy) {
    options.target.top += snapTo.dy;
  }
  this.fire("object:snapto", { e: snapTo });
}
return snapTo;
};

fabric.util.object.extend(fabric.Object.prototype, {
snappable: true
});

fabric.util.object.extend(fabric.Canvas.prototype, {
tolerance: 10,
renderSnappingHelperLines: true,
setSnapping: function setSnapping(val) {
  function _renderSnapping() {
    if (this.renderSnappingHelperLines && this.snapTo) {
      this.renderSnapping(this.snapTo, "#ffaaaa");
    }
  }

  if (val) {
    this.on("object:moving", this.gridSnapMoveWrapper);
    this.on('mouse:up', this.clearSnapping);
    this.on('after:render', _renderSnapping);
  } else {
    this.off("object:moving", this.gridSnapMoveWrapper);
    this.off('mouse:up', this.clearSnapping);
    this.off('after:render', _renderSnapping);
  }
  this.snapping = val;
},

snapping: false,
clearSnapping: function clearSnapping() {
  delete this.__snapCache;
  this.clearContext(this.contextTop);
  this.snapTo = null;
},


/**
 * @param object
 * @returns {{x: (false|{value, corner}|{value, corner, object2, corner2}), y: (false|{value, corner}|{value, corner, object2, corner2})}}
 */
gridSnapMoveWrapper: function gridSnapMoveWrapper(options) {

    if(typeof Symbol === 'undefined') {return;}

  if (options.e.shiftKey || !options.target.snappable) {
    return;
  }

  var snapObjects = [{ cx: this.originalWidth / 2 }, { cy: this.originalHeight / 2 }];
  if (this.supportLines) {
    var _iteratorNormalCompletion14 = true;
    var _didIteratorError14 = false;
    var _iteratorError14 = undefined;

    try {
      for (var _iterator14 = this.supportLines[Symbol.iterator](), _step14; !(_iteratorNormalCompletion14 = (_step14 = _iterator14.next()).done); _iteratorNormalCompletion14 = true) {
        var l = _step14.value;

        snapObjects.push(l.x ? { instance: l, x: l.x } : { instance: l, y: l.y });
      }
    } catch (err) {
      _didIteratorError14 = true;
      _iteratorError14 = err;
    } finally {
      try {
        if (!_iteratorNormalCompletion14 && _iterator14.return) {
          _iterator14.return();
        }
      } finally {
        if (_didIteratorError14) {
          throw _iteratorError14;
        }
      }
    }
  }
  if (this.guidlines) {
    var _iteratorNormalCompletion15 = true;
    var _didIteratorError15 = false;
    var _iteratorError15 = undefined;

    try {
      for (var _iterator15 = this.guidlines[Symbol.iterator](), _step15; !(_iteratorNormalCompletion15 = (_step15 = _iterator15.next()).done); _iteratorNormalCompletion15 = true) {
        var _l = _step15.value;

        snapObjects.push(_l.x ? { instance: _l, x: _l.x } : { instance: _l, y: _l.y });
      }
    } catch (err) {
      _didIteratorError15 = true;
      _iteratorError15 = err;
    } finally {
      try {
        if (!_iteratorNormalCompletion15 && _iterator15.return) {
          _iterator15.return();
        }
      } finally {
        if (_didIteratorError15) {
          throw _iteratorError15;
        }
      }
    }
  }

  this.snapTo = this.gridSnapMove({
    tolerance: this.tolerance,
    guidlines: snapObjects,
    objects: this._objects,
    target: options.target,
    area: {
      x1: this.offsets.left,
      y1: this.offsets.top,
      x2: this.originalWidth - this.offsets.right,
      y2: this.originalHeight - this.offsets.bottom
    },
    grid: this.grid && {
      offsetX: this.offsets.left - this.grid._gridOffset.x,
      offsetY: this.offsets.top - this.grid._gridOffset.y,
      size: this.gridSize
    }
  });
}
});
//End: Snapping

fabric.Text.prototype._constrainScale = function (value) {

  if (Math.abs(value) < this.minScaleLimit) {
    if (value < 0) {
      return -this.minScaleLimit;
    } else {
      return this.minScaleLimit;
    }
  }

  //FPD: minimum font size
  if (this.minFontSize !== undefined) {

    var scaledFontSize = parseFloat(Number(value * this.fontSize).toFixed(0));
    if (scaledFontSize < this.minFontSize) {
      return this.minFontSize / this.fontSize;
    }
  }

  //FPD: maximum font size
  if (this.maxFontSize !== undefined) {

    var scaledFontSize = parseFloat(Number(value * this.fontSize).toFixed(0));
    if (scaledFontSize > this.maxFontSize) {
      return this.maxFontSize / this.fontSize;
    }
  }

  return value;
};

//---- Modify tspan in SVG otherwise text styles are not displayed in PDF
fabric.util.object.extend(fabric.Text.prototype, {

  _createTextCharSpan: function _createTextCharSpan(_char, styleDecl, left, top) {

    //FPD: add text styles to tspan
    styleDecl.fontWeight = this.fontWeight;
    styleDecl.fontStyle = this.fontStyle;

    var shouldUseWhitespace = _char !== _char.trim() || _char.match(/  +/g),
      styleProps = this.getSvgSpanStyles(styleDecl, shouldUseWhitespace);

    //FPD: add underlined text
    styleProps += this.textDecoration === 'underline' ? ' text-decoration: underline;' : '';

    var fillStyles = styleProps ? 'style="' + styleProps + '"' : '',
      dy = styleDecl.deltaY,
      dySpan = '',
      NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;

    if (dy) {
      dySpan = ' dy="' + fabric.util.toFixed(dy, NUM_FRACTION_DIGITS) + '" ';
    }

    return ['<tspan x="', fabric.util.toFixed(left, NUM_FRACTION_DIGITS), '" y="', fabric.util.toFixed(top, NUM_FRACTION_DIGITS), '" ', dySpan, fillStyles, '>', fabric.util.string.escapeXml(_char), '</tspan>'].join('');
  }
});



/**
* modified to use with `offsetCorner` property
* @param absolute
* @returns {{tl: *|fabric.Point, tr: *|fabric.Point, br: *|fabric.Point, bl: *|fabric.Point}}
*/
fabric.Object.prototype._fpdBasicCalcCoords = function (absolute) {

	var multiplyMatrices = fabric.util.multiplyTransformMatrices,
	  transformPoint = fabric.util.transformPoint,
	  degreesToRadians = fabric.util.degreesToRadians,
	  rotateMatrix = this._calcRotateMatrix(),
	  translateMatrix = this._calcTranslateMatrix(),
	  startMatrix = multiplyMatrices(translateMatrix, rotateMatrix),
	  vpt = this.getViewportTransform(),
	  finalMatrix = absolute ? startMatrix : multiplyMatrices(vpt, startMatrix),
	  dim = this._getTransformedDimensions(),
	  w = dim.x / 2,
	  h = dim.y / 2,
	  tl = transformPoint({
	    x: -w,
	    y: -h
	  }, finalMatrix),
	  tr = transformPoint({
	    x: w,
	    y: -h
	  }, finalMatrix),
	  bl = transformPoint({
	    x: -w,
	    y: h
	  }, finalMatrix),
	  br = transformPoint({
	    x: w,
	    y: h
	  }, finalMatrix);


	//FPD: modified to use with `offsetCorner` property
	if(this.offsetCorner){
	  var cornerCenterW = dim.x / 2 + this.offsetCorner,
	    cornerCenterH = dim.y / 2 + this.offsetCorner;
	  tl._corner = transformPoint({x: -cornerCenterW, y: -cornerCenterH}, finalMatrix);
	  tr._corner = transformPoint({x:   cornerCenterW, y: -cornerCenterH}, finalMatrix);
	  bl._corner = transformPoint({x: -cornerCenterW, y: cornerCenterH}, finalMatrix);
	  br._corner = transformPoint({x: cornerCenterW, y: cornerCenterH}, finalMatrix);
	}
	//FPD: end

	if (!absolute) {
	  if (this.padding) {

	    var padding = this.padding,
	      angle = degreesToRadians(this.angle),
	      cos = fabric.util.cos(angle),
	      sin = fabric.util.sin(angle),
	      cosP = cos * padding,
	      sinP = sin * padding,
	      cosPSinP = cosP + sinP,
	      cosPMinusSinP = cosP - sinP;

	    tl.x -= cosPMinusSinP;
	    tl.y -= cosPSinP;
	    tr.x += cosPSinP;
	    tr.y -= cosPMinusSinP;
	    bl.x -= cosPSinP;
	    bl.y += cosPMinusSinP;
	    br.x += cosPMinusSinP;
	    br.y += cosPSinP;

	    //FPD: modified to use with `offsetCorner` property
	    if(this.offsetCorner) {
	      tl._corner.x -= cosPMinusSinP;
	      tl._corner.y -= cosPSinP;
	      tr._corner.x += cosPSinP;
	      tr._corner.y -= cosPMinusSinP;
	      bl._corner.x -= cosPSinP;
	      bl._corner.y += cosPMinusSinP;
	      br._corner.x += cosPMinusSinP;
	      br._corner.y += cosPSinP;
	    }
	    //FPD: end
	  }

	  var ml = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2),
	    mt = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2),
	    mr = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2),
	    mb = new fabric.Point((br.x + bl.x) / 2, (br.y + bl.y) / 2),

	    //FPD: Adjust calculation for top/right position
	    mtrX = tr.x,
	    mtrY = tr.y,
	    mtr = new fabric.Point(mtrX + sin * this.rotatingPointOffset, mtrY - cos * this.rotatingPointOffset);

	  // modified to use with `offsetCorner` property
	  if(this.offsetCorner) {
	    mtr._corner = new fabric.Point(tr._corner.x, tr._corner.y);
	  }
	  //FPD: end
	}

	var coords = {
	  tl: tl,
	  tr: tr,
	  br: br,
	  bl: bl
	};

	if (!absolute) {
	  coords.ml = ml;
	  coords.mt = mt;
	  coords.mr = mr;
	  coords.mb = mb;
	  coords.mtr = mtr;
	}

	return coords;
};


/**
* Sets the coordinates of the draggable boxes in the corners of
* the image used to scale/rotate it.
* Edited : modified to use with `offsetCorner` property
* @private
*/
fabric.Object.prototype._fpdBasicsetCornerCoords = function() {
	var coords = this.oCoords,
	  newTheta = fabric.util.degreesToRadians(45 - this.angle),
	  cornerHypotenuse = this.cornerSize * 0.707106,
	  cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta),
	  sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta),
	  x, y;

	for (var point in coords) {
	  //modified to use with `offsetCorner` property
	  if(coords[point]._corner){
	    x = coords[point]._corner.x;
	    y = coords[point]._corner.y;
	  }else{
	    x = coords[point].x;
	    y = coords[point].y;
	  }


	  coords[point].corner = {
	    tl: {
	      x: x - sinHalfOffset,
	      y: y - cosHalfOffset
	    },
	    tr: {
	      x: x + cosHalfOffset,
	      y: y - sinHalfOffset
	    },
	    bl: {
	      x: x - cosHalfOffset,
	      y: y + sinHalfOffset
	    },
	    br: {
	      x: x + sinHalfOffset,
	      y: y + cosHalfOffset
	    }
	  };
	}
};


fabric.Canvas.prototype._fpdBasicgetRotatedCornerCursor = function (corner, target, e) {
  var n = Math.round(target.angle % 360 / 45);

  //FPD: add CursorOffset
  var cursorOffset = {
    mt: 0, // n
    tr: 1, // ne
    mr: 2, // e
    br: 3, // se
    mb: 4, // s
    bl: 5, // sw
    ml: 6, // w
    tl: 7 // nw
  };

  if (n < 0) {
    n += 8; // full circle ahead
  }
  n += cursorOffset[corner];
  n %= 8;

  //FPD: set cursor for copy and remove
  switch (corner) {
    case 'tl':
      return target.copyable ? 'copy' : 'default';
      break;
    case 'bl':
      return 'pointer';
      break;
  }
  return this.cursorMap[n];
};

/**
 * modified to use with `offsetCorner` property
 * @param control
 * @param ctx
 * @param methodName
 * @param left
 * @param top
 * @private
 */
fabric.Object.prototype._fpdBasicdrawControl = function (control, ctx, methodName, left, top) {
  var size = this.cornerSize,
    iconOffset = 4,
    iconSize = size - iconOffset * 2,
    offsetCorner = this.offsetCorner,
    dotSize = 4,
    icon = false;


  if (this.isControlVisible(control)) {

    var wh = this._calculateCurrentDimensions(),
      width = wh.x,
      height = wh.y;

    if (control == 'br' || control == 'mtr' || control == 'tl' || control == 'bl' || control == 'ml' || control == 'mr' || control == 'mb' || control == 'mt') {
      switch (control) {

        case 'tl':
          //copy
          left = left -  offsetCorner;
          top = top -  offsetCorner;
          icon = this.__editorMode || this.copyable ? String.fromCharCode('0xe942') : false;
          break;
        case 'mtr':
          // rotate
          var rotateRight = width / 2;
          left = left + rotateRight +  offsetCorner;
          top = top -  offsetCorner;
          icon = (this.__editorMode || this.rotatable) ? String.fromCharCode('0xe923') : false;
          break;
        case 'br':
          // resize
          left = left +  offsetCorner;
          top = top +  offsetCorner;
          icon = (this.__editorMode || this.resizable) && this.type !== 'textbox' ? String.fromCharCode('0xe922') : false;
          break;
        case 'bl':
          //remove
          left = left -  offsetCorner;
          top = top +  offsetCorner;
          icon = this.__editorMode || this.removable ? String.fromCharCode('0xe926') : false;
          break;
      }
    }

    this.transparentCorners || ctx.clearRect(left, top, size, size);

    var extraLeftOffset = control == 'mt' || control == 'mb' ? 5 : 0;
    ctx.fillStyle = this.cornerColor;

   if (((control == 'ml' || control == 'mr') && !this.lockScalingX) || ((control == 'mt' || control == 'mb') && !this.lockScalingY)) {
	    ctx.beginPath();
	    left += dotSize * 3;
	    top += dotSize * 3;
	    ctx.arc(left, top, dotSize, 0, 2 * Math.PI);
	    ctx.fillStyle = this.cornerIconColor;
	    ctx.fill();
	  }
	  else if(icon) {
	    ctx.fillRect(left, top, size, size);
	    ctx.font = iconSize + 'px FontFPD';
	    ctx.fillStyle = this.cornerIconColor;
	    ctx.textAlign = 'left';
	    ctx.textBaseline = 'top';
	    ctx.fillText(icon, left + iconOffset + extraLeftOffset, top + iconOffset);
	  }

  }
};

var FPDPathGroupName = fabric.version === '1.6.7' ? 'path-group' : 'group';

/**
 * A class with some static helper functions. You do not need to initiate the class, just call the methods directly, e.g. FPDUtil.isIE();
 *
 * @class FPDUtil
 */
var FPDUtil =  {

	/**
	 * Checks if browser is IE and return version number.
	 *
	 * @method isIE
	 * @return {Boolean} Returns true if browser is IE.
	 * @static
	 */
	isIE : function() {

		var myNav = navigator.userAgent.toLowerCase();
		return (myNav.indexOf('msie') !== -1) ? parseInt(myNav.split('msie')[1]) : false;

	},

	/**
	 * Resets the key names of the deprecated keys.
	 *
	 * @method rekeyDeprecatedKeys
	 * @param {Object} object An object containing element parameters.
	 * @return {Object} Returns the edited object.
	 * @static
	 */
	rekeyDeprecatedKeys : function(object) {

		var depractedKeys = [
			{old: 'x', replace: 'left'},
			{old: 'y', replace: 'top'},
			{old: 'degree', replace: 'angle'},
			{old: 'currentColor', replace: 'fill'},
			{old: 'filters', replace: 'availableFilters'},
			{old: 'textSize', replace: 'fontSize'},
			{old: 'font', replace: 'fontFamily'},
			{old: 'scale', replace: ['scaleX', 'scaleY']},
			{old: 'uploadZoneScaleMode', replace: 'scaleMode'},
		];

		for(var i=0; i < depractedKeys.length; ++i) {
			if(object.hasOwnProperty(depractedKeys[i].old) && !object.hasOwnProperty(depractedKeys[i].replace)) {

				var replaceObj = depractedKeys[i].replace;
				//this.log('FPD 4.0.0: Parameter "'+depractedKeys[i].old+'" is depracted. Please use "'+replaceObj.toString()+'" instead!', 'warn');

				if(typeof replaceObj === 'object') { //check if old needs to be replaced with multiple options, e.g. scale=>scaleX,scaleY

					for(var j=0; j < replaceObj.length; ++j) {
						object[replaceObj[j]] = object[depractedKeys[i].old];
					}

				}
				else {
					object[depractedKeys[i].replace] = object[depractedKeys[i].old];
				}

				delete object[depractedKeys[i].old];
			}
		}

		return object;

	},

	/**
	 * Writes a message in the console.
	 *
	 * @method log
	 * @param {String} message The text that will be displayed in the console.
	 * @param {String} [type=log] The output type - info, error, warn or log.
	 * @static
	 */
	log : function(message, type) {

		if(typeof console === 'undefined') { return false; }

		if(type === 'info') {
			console.info(message);
		}
		else if (type === 'error') {
			console.error(message);
		}
		else if (type === 'warn') {
			console.warn(message);
		}
		else {
			console.log(message);
		}

	},

	/**
	 * Checks if a string is an URL.
	 *
	 * @method isUrl
	 * @param {String} s The string.
	 * @return {Boolean} Returns true if string is an URL.
	 * @static
	 */
	isUrl : function(s) {

		var regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
		return regexp.test(s);

	},

	/**
	 * Removes an element from an array by value.
	 *
	 * @method removeFromArray
	 * @param {Array} array The target array.
	 * @param {String} element The element value.
	 * @return {Array} Returns the edited array.
	 * @static
	 */
	removeFromArray : function(array, element) {

	    var index = array.indexOf(element);
	    if (index > -1) {
		    array.splice(index, 1);
		}

		return array;

	},

	/**
	 * Checks if a string is XML formatted.
	 *
	 * @method isXML
	 * @param {String} string The target string.
	 * @return {Boolean} Returns true if string is XML formatted.
	 * @static
	 */
	isXML : function(string){

	    try {
	        xmlDoc = jQuery.parseXML(string); //is valid XML
	        return true;
	    } catch (err) {
	        // was not XML
	        return false;
	    }

	},

	/**
	 * Checks if an image can be colorized and returns the image type
	 *
	 * @method elementIsColorizable
	 * @param {fabric.Object} element The target element.
	 * @return {String | Boolean} Returns the element type(text, dataurl, png or svg) or false if the element can not be colorized.
	 * @static
	 */
	elementIsColorizable : function(element) {

		if(this.getType(element.type) === 'text') {
			return 'text';
		}

		if(!element.source) {
			return false;
		}

		//check if url is a png or base64 encoded
		var imageParts = element.source.split('.');
		//its base64 encoded
		if(imageParts.length == 1) {

			//check if dataurl is png
			if(imageParts[0].search('data:image/png;') == -1) {
				element.fill = element.colors = false;
				return false;
			}
			else {
				return 'dataurl';
			}

		}
		//its a url
		else {

			var source = element.source;

			source = source.split('?')[0];//remove all url parameters
			imageParts = source.split('.');

			//only png and svg are colorizable
			if(jQuery.inArray('png', imageParts) == -1 && !FPDUtil.isSVG(element)) {
				element.fill = element.colors = false;
				return false;
			}
			else {
				if(FPDUtil.isSVG(element)) {
					return 'svg';
				}
				else {
					return 'png';
				}
			}

		}

	},

	/**
	 * Returns a simpler type of a fabric object.
	 *
	 * @method getType
	 * @param {String} fabricType The fabricjs type.
	 * @return {String} This could be image or text.
	 * @static
	 */
	getType : function(fabricType) {

		if(fabricType === 'text' || fabricType === 'i-text' || fabricType === 'curvedText' || fabricType === 'textbox') {
			return 'text';
		}
		else {
			return 'image';
		}

	},

	/**
	 * Looks for the .fpd-tooltip classes and adds a nice tooltip to these elements (tooltipster).
	 *
	 * @method updateTooltip
	 * @param {jQuery} [$container] The container to look in. If not set, the whole document will be searched.
	 * @static
	 */
	updateTooltip : function($container) {

		var $tooltips = $container ? $container.find('.fpd-tooltip') : jQuery('.fpd-tooltip');

		$tooltips.each(function(i, tooltip) {

			var $tooltip = jQuery(tooltip);
			if($tooltip.hasClass('tooltipstered')) {
				$tooltip.tooltipster('reposition');
			}
			else {
				$tooltip.tooltipster({
					offsetY: 0,
					position: 'bottom',
					theme: '.fpd-tooltip-theme',
					touchDevices: false
				});
			}

		});

	},

	/**
	 * Makes an unique array.
	 *
	 * @method arrayUnique
	 * @param {Array} array The target array.
	 * @return {Array} Returns the edited array.
	 * @static
	 */
	arrayUnique : function(array) {

	    var a = array.concat();
	    for(var i=0; i<a.length; ++i) {
	        for(var j=i+1; j<a.length; ++j) {
	            if(a[i] === a[j])
	                a.splice(j--, 1);
	        }
	    }

	    return a;
	},

	/**
	 * Creates a nice scrollbar for an element.
	 *
	 * @method createScrollbar
	 * @param {jQuery} target The target element.
	 * @static
	 */
	createScrollbar : function($target, axis) {

		axis = axis === undefined ? 'y' : axis;

		if($target.hasClass('mCustomScrollbar')) {
			$target.mCustomScrollbar('update');
			$target.mCustomScrollbar('scrollTo', 0);
		}
		else {
			$target.mCustomScrollbar({
				scrollbarPosition: 'outside',
				autoExpandScrollbar: true,
				autoHideScrollbar: false,
				scrollInertia: 200,
				axis: axis,
				keyboard: {enable: false},
				mouseWheel: {
					preventDefault: true
				},
				callbacks: {
					onTotalScrollOffset: 100,
					onTotalScroll:function() {
						jQuery(this).trigger('_sbOnTotalScroll');
						FPDUtil.refreshLazyLoad(jQuery(this).find('.fpd-grid'), true);
					}
				}
			});
		}

	},

	/**
	 * Checks if a value is not empty. 0 is allowed.
	 *
	 * @method notEmpty
	 * @param {Number | String} value The target value.
	 * @return {Array} Returns true if not empty.
	 * @static
	 */
	notEmpty : function(value) {

		if(value === undefined || value === false || value.length === 0) {
			return false;
		}
		return true;

	},

	/**
	 * Opens the modal box with an own message.
	 *
	 * @method showModal
	 * @param {String} message The message you would like to display in the modal box.
	 * @return {jQuery} Returns a jQuery object containing the modal.
	 * @static
	 */
	showModal : function(htmlMessage, fullscreen, type, $container) {

		type = type === undefined ? '' : type;
		$container = $container === undefined ? jQuery('body') : $container;

		if($container.is('body')) {
			$container.addClass('fpd-overflow-hidden')
		}

		var fullscreenCSS = fullscreen ? 'fpd-fullscreen' : '',
			html = '<div class="fpd-modal-internal fpd-modal-overlay"><div class="fpd-modal-wrapper fpd-shadow-3"><div class="fpd-modal-close"><span class="fpd-icon-close"></span></div><div class="fpd-modal-content"></div></div></div>';

		if(jQuery('.fpd-modal-internal').length === 0) {

			$container.append(html)
			.children('.fpd-modal-internal:first').click(function(evt) {

				var $target = jQuery(evt.target);
				if($target.hasClass('fpd-modal-overlay')) {

					$target.find('.fpd-modal-close').click();

				}

			});

		}

		if(type === 'prompt') {
			htmlMessage = '<input type="text" placeholder="'+htmlMessage+'" /><span class="fpd-btn"></span>';
		}
		else if(type === 'confirm') {
			htmlMessage = '<div class="fpd-confirm-msg">'+htmlMessage+'</div><span class="fpd-btn fpd-confirm"></span>';
		}

		$container.children('.fpd-modal-internal').attr('data-type', type).removeClass('fpd-fullscreen').addClass(fullscreenCSS)
		.fadeIn(300).find('.fpd-modal-content').html(htmlMessage);

		return $container.children('.fpd-modal-internal');

	},

	/**
	 * Shows a message in the snackbar.
	 *
	 * @method showMessage
	 * @param {String} text The text for the message.
	 * @static
	 */
	showMessage : function(text, autoRemove) {

		autoRemove = autoRemove === undefined ? true : autoRemove;

		var $body = jQuery('body'),
			$snackbarWrapper;

		if($body.children('.fpd-snackbar-wrapper').length > 0) {
			$snackbarWrapper = $body.children('.fpd-snackbar-wrapper');
		}
		else {
			$snackbarWrapper = $body.append('<div class="fpd-snackbar-wrapper"></div>').children('.fpd-snackbar-wrapper');
		}

		var $snackbar = jQuery('<div class="fpd-snackbar fpd-shadow-1"><p></p></div>');
		$snackbar.children('p').html(text);
		$snackbar.appendTo($snackbarWrapper);

		setTimeout(function() {

			$snackbar.addClass('fpd-show-up');

			if(autoRemove) {
				setTimeout(function() {
					$snackbar.remove();
				}, 5000);
			}


		}, 10);

		return $snackbar;

	},

	/**
	 * Adds a preloader icon to loading picture and loads the image.
	 *
	 * @method loadGridImage
	 * @param {jQuery} picture The image container.
	 * @param {String} source The image URL.
	 * @static
	 */
	loadGridImage : function($picture, source) {

		if($picture.length > 0 && source) {

			$picture.addClass('fpd-on-loading');
			var image = new Image();
			image.src = source;
			image.onload = function() {
				$picture.data('originWidth', this.width).data('originHeight', this.height)
				.removeClass('fpd-on-loading').fadeOut(0)
				.stop().fadeIn(200).css('background-image', 'url("'+this.src+'")');
			};
			image.onerror = function() {

				$picture.parent('.fpd-item').remove();

			}

		}

	},

	//
	/**
	 * Refreshs the items using lazy load.
	 *
	 * @method refreshLazyLoad
	 * @param {jQuery} container The container.
	 * @param {Boolean} loadByCounter If true 15 images will be loaded at once. If false all images will be loaded in the container.
	 * @static
	 */
	refreshLazyLoad : function($container, loadByCounter) {

		if($container && $container.length > 0 /* && $container.is(':visible') */) {

			var $item = $container.children('.fpd-item.fpd-hidden:first'),
				counter = 0,
				amount = loadByCounter ? 15 : 0;

			while(
				(counter < amount
					|| $container.parent('.mCSB_container').height()-150 < $container.parents('.fpd-scroll-area:first').height())
				&& $item.length > 0
			) {
				var $pic = $item.children('picture');
				$item.removeClass('fpd-hidden');
				FPDUtil.loadGridImage($pic, $pic.data('img'));
				$item = $item.next('.fpd-item.fpd-hidden');
				counter++;
			}

		}

	},

	/**
	 * Parses the fabricjs options to a FPD options object.
	 *
	 * @method parseFabricObjectToFPDElement
	 * @param {Object} object The target fabricjs object.
	 * @return {Object} Returns the FPD object.
	 * @static
	 */
	parseFabricObjectToFPDElement : function(object) {

		if(!object) { return {}; }

		var options = new FancyProductDesignerOptions(),
			properties = Object.keys(options.defaults.elementParameters),
			additionalKeys  = FPDUtil.getType(object.type) === 'text' ? Object.keys(options.defaults.textParameters) : Object.keys(options.defaults.imageParameters);

		properties = jQuery.merge(properties, additionalKeys);

		var parameters = {};
		for(var i=0; i < properties.length; ++i) {
			var prop = properties[i];
			if(object[prop] !== undefined) {
				parameters[prop] = object[prop];
			}

		}

		return {
			type: FPDUtil.getType(object.type), //type
			source: object.source, //source
			title: object.title,  //title
			parameters: parameters  //parameters
		};

	},

	/**
	 * If pop-up blocker is enabled, the user will get a notification modal.
	 *
	 * @method popupBlockerAlert
	 * @param {window} popup The target popup window.
	 * @static
	 */
	popupBlockerAlert : function(popup, fpdInstance) {

		if (popup == null || typeof(popup)=='undefined') {
			FPDUtil.showModal(fpdInstance.getTranslation('misc', 'popup_blocker_alert'));
		}

	},

	/**
	 * Returns the scale value calculated with the passed image dimensions and the defined "resize-to" dimensions.
	 *
	 * @method getScalingByDimesions
	 * @param {Number} imgW The width of the image.
	 * @param {Number} imgH The height of the image.
	 * @param {Number} resizeToW The maximum width for the image.
	 * @param {Number} resizeToH The maximum height for the image.
	 * @return {Number} The scale value to resize an image to a desired dimension.
	  * @static
	 */
	getScalingByDimesions : function(imgW, imgH, resizeToW, resizeToH, mode) {

		mode = typeof mode === 'undefined' ? 'fit' : mode;
		resizeToW = typeof resizeToW !== 'number' ? 0 : resizeToW;
		resizeToH = typeof resizeToH !== 'number' ? 0 : resizeToH;

		var scaling = 1,
			rwSet = resizeToW !== 0,
			rhSet = resizeToH !== 0;

		if(mode === 'cover') { //cover whole area

			var dW = resizeToW - imgW,
				dH =  resizeToH - imgH;

		    if (dW < dH) { //scale width
		    	scaling = rwSet ? Math.max(resizeToW / imgW,  resizeToH / imgH) : 1;
		    }
		    else { //scale height
		      	scaling = rhSet ? Math.max(resizeToW / imgW,  resizeToH / imgH) : 1;
		    }

		}
		else { //fit into area

			if(imgW > imgH) {
				scaling = rwSet ? Math.min(resizeToW / imgW,  resizeToH / imgH) : 1;
			}
			else {
				scaling = rhSet ? Math.min(resizeToW / imgW,  resizeToH / imgH) : 1;
			}

		}

		return parseFloat(scaling.toFixed(10));

	},

	/**
	 * Checks if the browser local storage is available.
	 *
	 * @method localStorageAvailable
	 * @return {Boolean} Returns true if local storage is available.
	 * @static
	 */
	localStorageAvailable : function() {

		var localStorageAvailable = true;
		//execute this because of a ff issue with localstorage
		try {
			window.localStorage.length;
			window.localStorage.setItem('fpd-storage', 'just-testing');
			//window.localStorage.clear();
		}
		catch(error) {
			localStorageAvailable = false;
			//In Safari, the most common cause of this is using "Private Browsing Mode". You are not able to save products in your browser.
		}

		return localStorageAvailable;

	},

	/**
	 * Checks if the dimensions of an image is within the allowed range set in the customImageParameters of the view options.
	 *
	 * @method checkImageDimensions
	 * @param {FancyProductDesigner} fpdInstance Instance of FancyProductDesigner.
	 * @param {Number} imageW The image width.
	 * @param {Number} imageH The image height.
	 * @return {Array} Returns true if image dimension is within allowed range(minW, minH, maxW, maxH).
	 * @static
	 */
	checkImageDimensions : function(fpdInstance, imageW, imageH) {

		var imageRestrictions = fpdInstance.currentViewInstance.options.customImageParameters;

		var uploadZone = fpdInstance.currentViewInstance.getUploadZone(fpdInstance.currentViewInstance.currentUploadZone);
		if(uploadZone) {
			imageRestrictions = $.extend({}, imageRestrictions, uploadZone);
		}

		if(imageW > imageRestrictions.maxW ||
		imageW < imageRestrictions.minW ||
		imageH > imageRestrictions.maxH ||
		imageH < imageRestrictions.minH) {

			fpdInstance._loadingCustomImage = false;

			if(fpdInstance.mainBar) {
				fpdInstance.mainBar.toggleDialog(false);

				if(fpdInstance.currentViewInstance.currentUploadZone) {
					fpdInstance.mainBar.toggleUploadZonePanel(false);
				}

			}

			var msg = fpdInstance.getTranslation('misc', 'uploaded_image_size_alert')
					  .replace('%minW', imageRestrictions.minW)
					  .replace('%minH', imageRestrictions.minH)
					  .replace('%maxW', imageRestrictions.maxW)
					  .replace('%maxH', imageRestrictions.maxH);

			FPDUtil.showModal(msg);
			return false;

		}
		else {
			return true;
		}

	},

	/**
	 * Checks if an element has a color selection.
	 *
	 * @method elementHasColorSelection
	 * @param {fabric.Object} element The target element.
	 * @return {Boolean} Returns true if element has colors.
	 * @static
	 */
	elementHasColorSelection : function(element) {

		return (Array.isArray(element.colors) || Boolean(element.colors) || element.colorLinkGroup || element.__editorMode) && FPDUtil.elementIsColorizable(element) !== false;

	},

	/**
	 * Returns the available colors of an element.
	 *
	 * @method elementAvailableColors
	 * @param {fabric.Object} element The target element.
	 * @param {FancyProductDesigner} fpdInstance Instance of FancyProductDesigner.
	 * @return {Array} Available colors.
	 * @static
	 */
	elementAvailableColors : function(element, fpdInstance) {

		var availableColors = [];
		if(element.type == FPDPathGroupName) {

			var paths = element.getObjects();
			if(paths.length === 1) {
				availableColors = element.colors;
			}
			else {
				availableColors = [];
				for(var i=0; i < paths.length; ++i) {
					var path = paths[i],
						color = tinycolor(path.fill);
					availableColors.push(color.toHexString());
				}
			}

		}
		else if(element.colorLinkGroup && fpdInstance.colorLinkGroups[element.colorLinkGroup]) {
			availableColors = fpdInstance.colorLinkGroups[element.colorLinkGroup].colors;
		}
		else {
			availableColors = element.colors;
		}

		return availableColors;

	},

	/**
	 * Changes a single path color by index.
	 *
	 * @method changePathColor
	 * @param {fabric.Object} element The target element.
	 * @param {Number} index The path index.
	 * @param {String} color Hexadecimal color value.
	 * @return {Array} All colors used in the SVG.
	 * @static
	 */
	changePathColor : function(element, index, color) {

		var svgColors = [],
			paths = element.getObjects();

		for(var i=0; i < paths.length; ++i) {

			var path = paths[i],
				c = tinycolor(path.fill);

			svgColors.push(c.toHexString());
		}

		svgColors[index] = typeof color === 'string' ? color : color.toHexString();

		return svgColors;

	},

	/**
	 * Checks if a string is a valid hexadecimal color value.
	 *
	 * @method isHex
	 * @param {String} value The target value.
	 * @return {Boolean} Returns true if value is a valid hexadecimal color.
	 * @static
	 */
	isHex : function(value) {
		return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(value);
	},

	/**
	 * Adds a thousand separator and returns it.
	 *
	 * @method addThousandSep
	 * @param {Number} n A numeric value.
	 * @return {String} Returns a string.
	 * @static
	 */
	addThousandSep : function(n){

	    var rx=  /(\d+)(\d{3})/;
	    return String(n).replace(/^\d+/, function(w){
	        while(rx.test(w)){
	            w= w.replace(rx, '$1'+thousandSeparator+'$2');
	        }
	        return w;
	    });

	},

	getFilter : function(type, opts) {

		if(typeof type !== 'string') {
			return null;
		}

		opts = opts === undefined ? {} : opts;
		type = type.toLowerCase();

		if(FPDFilters[type] && FPDFilters[type].array) {
			return new fabric.Image.filters.ColorMatrix({
				matrix: FPDFilters[type].array,
			});
		}

		switch(type) {
			case 'grayscale':
				return new fabric.Image.filters.Grayscale();
			break;
			case 'sepia':
				return new fabric.Image.filters.Sepia();
			break;
			case 'sepia2':
				return new fabric.Image.filters.Sepia2();
			break;
			case 'brightness':
				return new fabric.Image.filters.Brightness(opts);
			break;
			case 'contrast':
				return new fabric.Image.filters.Contrast(opts);
			break;
			case 'removewhite':
				return new fabric.Image.filters.RemoveColor(opts);
			break;
		}

		return null;

	},

	spectrumColorNames : function($spContainer, fpdInstance) {

		$spContainer.find('.sp-palette-container .sp-thumb-el').each(function(i, ci) {

			var color = ci.title,
				colorName = fpdInstance.mainOptions.hexNames[color.replace('#', '').toLowerCase()];

			jQuery(ci).attr('title', colorName ? colorName : color).addClass('fpd-tooltip');

			FPDUtil.updateTooltip($spContainer);

		});

	},

	getDeviceByScreenSize : function() {

		var windowWidth = jQuery(window).width();
		if(windowWidth < 568) {
			return 'smartphone';
		}
		else if(windowWidth > 568 && windowWidth <= 768) {
			return 'tablet';
		}
		else {
			return 'desktop'
		}

	},

	elementIsEditable : function(element) {

		return element &&
			(typeof element.colors === 'object' ||
			element.colors === true ||
			element.colors == 1 ||
			element.removable ||
			element.draggable ||
			element.resizable ||
			element.rotatable ||
			element.zChangeable ||
			element.advancedEditing ||
			element.editable ||
			element.uploadZone ||
			(element.colorLinkGroup && element.colorLinkGroup.length > 0) ||
			element.__editorMode
			);


	},

	hexToRgb : function(hex) {

	    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
	    return result ? {
	        r: parseInt(result[1], 16),
	        g: parseInt(result[2], 16),
	        b: parseInt(result[3], 16)
	    } : null;

	},

	unitToPixel : function(length, unit, dpi) {

		dpi = dpi === undefined ? 72 : dpi;

		var ppi = length * dpi;

		if(unit == 'cm') {
			return Math.round(ppi / 2.54);
		}
		else if(unit == 'mm') {
			return Math.round(ppi / 25.4);
		}
		else {
			return Math.round(ppi);
		}

	},

	pixelToUnit : function(pixel, unit, dpi) {

		dpi = dpi === undefined ? 72 : dpi;

		var inches = pixel / dpi;

		if(unit == 'cm') {
			return Math.round(inches * 2.54);
		}
		else if(unit == 'mm') {
			return Math.round(inches * 25.4);
		}
		else {
			return Math.round(inches);
		}

	},

	isSVG : function(element) {

		return element !== null && (element.type === FPDPathGroupName || element.d !== undefined || (element.source && jQuery.inArray('svg', element.source.split('.')) !== -1));

	},

	inRange: function (val1, val2, tolerance) {

		tolerance = tolerance === undefined ? 10 : tolerance;

		return Math.abs(val1 - val2) < tolerance;

    },

    objectHasKeys: function (obj, keys) {

		if(obj && typeof obj === 'object') {

			var hasAllKeys = true;
			for(var i=0; i < keys.length; ++i) {

				var key = keys[i];
				if(!obj.hasOwnProperty(key)) {
					hasAllKeys = false;
					break;
				}

			}

			return hasAllKeys;

		}
		else {
			return false;
		}

    },

    setItemPrice: function($item, fpdInstance) {

	    if(!fpdInstance.currentViewInstance)
	    	return;

	    var currentViewOptions = fpdInstance.currentViewInstance.options,
	    	price = null;

	    if(fpdInstance.currentViewInstance && fpdInstance.currentViewInstance.currentUploadZone
			&& $item.parents('.fpd-upload-zone-adds-panel').length > 0) {

			var uploadZone = fpdInstance.currentViewInstance.getUploadZone(fpdInstance.currentViewInstance.currentUploadZone);
			if(uploadZone && uploadZone.price) {
				price = uploadZone.price;
			}

		}

	    //thumbnails in images module
		if($item.parents('[data-module="images"]:first').length > 0 && price === null) {

			if(!isNaN($item.data('price'))) {
				price = $item.data('price');
			}
			else if(currentViewOptions && currentViewOptions.customImageParameters.price) {
				price = currentViewOptions.customImageParameters.price;
			}

		}
		//thumbnails in designs/products module
		else {

			if($item.data('parameters') && $item.data('parameters').price && price === null) {
				price = $item.data('parameters').price;
			}

		}

		$item.children('.fpd-price').toggle(Boolean(price)).html(price ? fpdInstance.formatPrice(price) : '')

    },

    isZero: function(value) {

	    return value === 0 || (typeof value === 'string' && value === "0");

    },

    isEmpty: function(value) {

	    if (value === undefined) {
			return true;
  		}

	    if (value == null) {
			return true;
  		}

  		if (typeof value === 'string' || Array.isArray(value)) {
			return !value.length;
  		}

  		if (typeof value === 'object') {
			return !Object.keys(value).length;
  		}

	    return false;

    },

    getBgCssFromElement: function(element) {

	    var currentFill = element.fill;

	    //fill: hex
		if(typeof currentFill === 'string') {
			return currentFill;
		}
		//fill: pattern or svg fill
		else if(typeof currentFill === 'object') {

			if(currentFill.source) { //pattern
				currentFill = currentFill.source.src;
				return 'url('+currentFill+')';
			}
			else { //svg has fill
				return currentFill[0];
			}

		}
		//element: svg
		else if(element.colors === true && element.type === FPDPathGroupName) {
			return tinycolor(element.getObjects()[0].fill);
		}
		//no fill, only colors set
		else if(currentFill === false && element.colors && element.colors[0]) {
			return element.colors[0];
		}

    },

    //smartphone (not tablets)
    isMobile : function() {

	    var check = false;
    (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
  		return check;

    },

    parseFontsToEmbed : function(fontItem, loadFromScript) {

	    var embedString = '';

	    loadFromScript = loadFromScript === undefined ? '' : loadFromScript;

		if(fontItem.hasOwnProperty('url')) {

			var fontFamily = fontItem.name,
				fontFormat = fontItem.url.search('.woff') !== -1 ? 'woff' : 'TrueType',
				fontURL = loadFromScript ? loadFromScript+fontItem.url : fontItem.url;

			fontFamily += ':n4'

			embedString += '@font-face {font-family:"'+fontItem.name+'"; font-style: normal; font-weight: normal; src:url("'+fontURL+'") format("'+fontFormat+'");}';

			if(fontItem.variants) {

				Object.keys(fontItem.variants).forEach(function(fv) {

					var ffVars = {
						'n7': 'font-style: normal; font-weight: bold;',
						'i4': 'font-style: italic; font-weight: normal;',
						'i7': 'font-style: italic; font-weight: bold;'
					};

					fontURL = loadFromScript ? loadFromScript+fontItem.variants[fv] : fontItem.variants[fv];


					embedString += '@font-face {font-family:"'+fontItem.name+'"; '+ffVars[fv]+' src:url("'+fontURL+'") format("'+fontFormat+'");}';

				})

				fontFamily += ','+Object.keys(fontItem.variants).toString();

			}

		}

		return embedString;

    },

    convertHexToRGBA : function(hexCode, opacity) {

	    var hex = hexCode.replace('#', '');

		if (hex.length === 3) {
	    	hex = "".concat(hex[0]).concat(hex[0]).concat(hex[1]).concat(hex[1]).concat(hex[2]).concat(hex[2]);
		}

		var r = parseInt(hex.substring(0, 2), 16);
		var g = parseInt(hex.substring(2, 4), 16);
	 	var b = parseInt(hex.substring(4, 6), 16);

	 	return "rgba(".concat(r, ",").concat(g, ",").concat(b, ",").concat(opacity / 100, ")");

    },

    getDataUriSize : function(dataURL, unit) {

	    unit = unit === undefined ? 'mb' : unit;

	    var base64String = dataURL.split(",")[1];
		var stringLength = base64String.length;
		var sizeInBytes = 4 * Math.ceil((stringLength / 3))*0.5624896334383812;

		if(unit == 'byte') {
			return sizeInBytes;
		}
		else if(unit == 'kb') {
			return sizeInBytes/1000;
		}
		else {
			return sizeInBytes/1000000;
		}

    },

    getFileExtension: function(str) {
	    //ext > lowercase > remove query params
	    return str.split('.').pop().toLowerCase().split('?')[0];
    },

	/**
	 * Changes the DPI of a base64 image.
	 *
	 * @method changeBase64DPI
	 * @param {dataURI} string A base64 data uri representing the image(png or jpeg).
	 * @param {dpi} number The target DPI.
	 * @return {String} Returns the base64 image with the new DPI.
	 * @static
	 */
    changeBase64DPI : function(dataURI, dpi) {

	    dpi = dpi === undefined ? 72 : dpi;

	    return dpi == 72 ? dataURI : changeDpiDataUrl(dataURI, dpi);

    }

};

var FPDEmojisRegex = /\uD83C\uDFF4(?:\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74)\uDB40\uDC7F|\u200D\u2620\uFE0F)|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC68(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDB0-\uDDB3])|(?:\uD83C[\uDFFB-\uDFFF])\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDB0-\uDDB3]))|\uD83D\uDC69\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDB0-\uDDB3])|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2695\u2696\u2708]|\uD83D\uDC68(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|(?:(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)\uFE0F|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDD6-\uDDDD])(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\u200D[\u2640\u2642])|\uD83D\uDC69\u200D[\u2695\u2696\u2708])\uFE0F|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D\uDC68(?:\u200D(?:(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D[\uDC66\uDC67])|\uD83C[\uDFFB-\uDFFF])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDB0-\uDDB3])|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF4\uD83C\uDDF2|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDB5\uDDB6\uDDD1-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDB5\uDDB6\uDDB8\uDDB9\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF9]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD70\uDD73-\uDD76\uDD7A\uDD7C-\uDDA2\uDDB0-\uDDB9\uDDC0-\uDDC2\uDDD0-\uDDFF])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF9]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD70\uDD73-\uDD76\uDD7A\uDD7C-\uDDA2\uDDB0-\uDDB9\uDDC0-\uDDC2\uDDD0-\uDDFF])\uFE0F/g;

/**
 * The class defining the default options for Fancy Product Designer.
 *
 * @class Options
 */
var FancyProductDesignerOptions = function() {

	/**
	 * The default options. See: {{#crossLink "Options.defaults"}}{{/crossLink}}
	 *
	 * @property defaults
	 * @for Options
	 * @type {Object}
	 */
	this.defaults = {
		imageLoadTimestamp: false,
	    /**
		* The stage(canvas) width for the product designer.
		*
		* @property stageWidth
		* @for Options.defaults
		* @type {Number}
		* @default "900"
		*/
		stageWidth: 900,
		/**
		* The stage(canvas) height for the product designer.
		*
		* @property stageHeight
		* @for Options.defaults
		* @type {Number}
		* @default "600"
		*/
		stageHeight: 600,
		/**
		* Enables the editor mode, which will add a helper box underneath the product designer with some options of the current selected element.
		*
		* @property editorMode
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		editorMode: false,
		/**
		* The properties that will be displayed in the editor box when an element is selected.
		*
		* @property editorBoxParameters
		* @for Options.defaults
		* @type {Array}
		* @default ['left', 'top', 'angle', 'fill', 'width', 'height', 'fontSize', 'price']
		*/
		editorBoxParameters: ['left', 'top', 'angle', 'fill', 'width', 'height', 'fontSize', 'price'],
		/**
		* An array containing all available fonts.<br/>Since V4.3 you can use TrueType fonts (ttf), which is also recommend. TrueType fonts are required to include the font in the PDF for Fancy Product Designer - Admin, see example.
		*
		* @property fonts
		* @for Options.defaults
		* @type {Aarray}
		* @default [{name: 'Arial'}, {name: 'Lobster', url: 'google'}]
		* @example <br />[{name: "Lobster", url: "google"}, {name: 'Custom', url: 'https://yourdomain.com/fonts/custom.ttf"}, {name: 'Aller', url: 'path/Aller.ttf', variants: {'n7': 'path/Aller__bold.ttf','i4': 'path/Aller__italic.ttf','i7': 'path/Aller__bolditalic.ttf'}}]
		*/
		fonts: [{name: 'Arial'}, {name: 'Lobster', url: 'google'}],
		/**
		* The directory path that contains the templates.
		*
		* @property templatesDirectory
		* @for Options.defaults
		* @type {String}
		* @default 'templates/'
		*/
		templatesDirectory: 'html/',
		/**
		* To add photos from Facebook, you have to set your own Facebook API key.
		*
		* @property facebookAppId
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		facebookAppId: '',
		/**
		* To add photos from Instagram, you have to set an <a href="http://instagram.com/developer/" target="_blank">Instagram client ID</a>.
		*
		* @property instagramClientId
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		instagramClientId: '', //the instagram client ID
		/**
		* This URI to the html/instagram_auth.html. You have to update this option if you are using a different folder structure.
		*
		* @property instagramRedirectUri
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		instagramRedirectUri: '',
		/**
		* The URI to the script that get the access token from Instagram. You need the enter the Instagram Secret ID.
		*
		* @property instagramTokenUri
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		instagramTokenUri: '',
		/**
		* The zoom step when using the UI slider to change the zoom level.
		*
		* @property zoomStep
		* @for Options.defaults
		* @type {Number}
		* @default 0.2
		*/
		zoomStep: 0.2,
		/**
		* The maximal zoom factor. Set it to 1 to hide the zoom feature in the user interface.
		*
		* @property maxZoom
		* @for Options.defaults
		* @type {Number}
		* @default 3
		*/
		maxZoom: 3,
		/**
		* Set custom names for your hexdecimal colors. key=hexcode without #, value: name of the color.
		*
		* @property hexNames
		* @for Options.defaults
		* @type {Object}
		* @default {}
		* @example hexNames: {000000: 'dark',ffffff: 'white'}
		*/
		hexNames: {},
		/**
		* The border color of the selected element.
		*
		* @property selectedColor
		* @for Options.defaults
		* @type {String}
		* @default '#d5d5d5'
		*/
		selectedColor: '#f5f5f5',
		/**
		* The border color of the bounding box.
		*
		* @property boundingBoxColor
		* @for Options.defaults
		* @type {String}
		* @default '#005ede'
		*/
		boundingBoxColor: '#2185d0',
		/**
		* The border color of the element when its outside of his bounding box.
		*
		* @property outOfBoundaryColor
		* @for Options.defaults
		* @type {String}
		* @default '#990000'
		*/
		outOfBoundaryColor: '#990000',
		/**
		* If true only the initial elements will be replaced when changing the product. Custom added elements will not be removed.
		*
		* @property replaceInitialElements
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		replaceInitialElements: false,
		/**
		* If true lazy load will be used for the images in the "Designs" module and "Change Product" module.
		*
		* @property lazyLoad
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		lazyLoad: true,
		/**
		* Defines the file type used for the templates. E.g. if you want to convert all template files (productdesigner.html and canvaserror.html) into PHP files, you need to change this option to 'php'.
		*
		* @property templatesType
		* @for Options.defaults
		* @type {String}
		* @default 'html'
		*/
		templatesType: 'html',
		/**
		* An object that contains the settings for the AJAX post when a custom added image is added to the canvas (Uploaded Images, Facebook/Instagram Photos). This allows to send the URL of the image to a custom-built script, that returns the data URI of the image or uploads the image to your server and returns the new URL on your server. By default the URL is send to php/custom-image-handler.php. See the <a href="http://api.jquery.com/jquery.ajax/" target="_blank">official jQuery.ajax documentation</a> for more information. The data object has a reserved property called url, which is the image URL that will send to the script. The success function is also reserved.
		*
		* @property customImageAjaxSettings
		* @for Options.defaults
		* @type {Object}
		* @example
		* <pre> customImageAjaxSettings: {<br />  url: 'src/php/custom-image-handler.php',<br />  data: {<br/>   saveOnServer: 1, //image is uploaded to your server <br/>   uploadsDir: '/path/to/uploads_dir', //into this directory <br/>   uploadsDirURL: 'http://yourdomain.com/uploads_dir' //and returns the new URL from this location <br />}}</pre>
		*/
		customImageAjaxSettings: {
			/**
			* The URL to the custom-image-handler.php
			*
			* @property url
			* @type {String}
			* @for Options.defaults.customImageAjaxSettings
			* @default 'php/custom-image-handler.php'
			*/
			url: 'php/custom-image-handler.php',
			/**
			* The HTTP method to use for the request.
			*
			* @property method
			* @type {String}
			* @for Options.defaults.customImageAjaxSettings
			* @default 'POST'
			*/
			method: 'POST',
			/**
			* The type of data that you're expecting back from the server.
			*
			* @property dataType
			* @type {String}
			* @for Options.defaults.customImageAjaxSettings
			* @default 'json'
			*/
			dataType: 'json',
			/**
			* The data object sent to the server.
			*
			* @property data
			* @type {Object}
			* @for Options.defaults.customImageAjaxSettings
			* @default {
				saveOnServer: 0, - use integer as boolean value. 0=false, 1=true
				uploadsDir: './uploads', - if saveOnServer is 1, you need to specify the directory path where the images are saved
				uploadsDirURL: 'http://yourdomain.com/uploads' - if saveOnServer is 1, you need to specify the directory URL where the images are saved
			}
			*/
			data: {
				saveOnServer: 0, //use integer as boolean value. 0=false, 1=true
				uploadsDir: './uploads', //if saveOnServer is true, you need to specify the directory path where the images are saved
				uploadsDirURL: 'http://yourdomain.com/uploads' //if saveOnServer is true, you need to specify the directory URL where the images are saved
			}
		},
		/**
		* Enable an improved resize filter, that may improve the image quality when its resized.
		*
		* @property improvedResizeQuality
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		improvedResizeQuality: false,
		/**
		* Make the canvas and the elements in the canvas responsive.
		*
		* @property responsive
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		responsive: true,
		/**
		* Hex color value defining the color for the corner icon controls.
		*
		* @property cornerIconColor
		* @for Options.defaults
		* @type {String}
		* @default '#000000'
		*/
		cornerIconColor: '#000000', //hex
		/**
		* The URL to the JSON file or an object containing all content from the JSON file. Set to false, if you do not need it.
		*
		* @property langJSON
		* @for Options.defaults
		* @type {String | Object | Boolean}
		* @default 'lang/default.json'
		*/
		langJSON: 'lang/default.json',
		/**
		* The color palette when the color wheel is displayed.
		*
		* @property colorPickerPalette
		* @for Options.defaults
		* @type {Array}
		* @default []
		* @example ['#000', '#fff']
		*/
		colorPickerPalette: [], //when colorpicker is enabled, you can define a default palette
		/**
		* An object defining the available actions in the different zones.
		*
		* @property actions
		* @for Options.defaults
		* @type {Object}
		* @default {'top': [], 'right': [], 'bottom': [], 'left': []}
		* @example {'top': ['manage-layers'], 'right': ['info'], 'bottom': ['undo', 'redo'], 'left': []}
		*/
		actions:  {
			'top': [],
			'right': [],
			'bottom': [],
			'left': []
		},
		/**
		* An array defining the available modules in the main bar. Possible values: 'products', 'images', 'text', 'designs'. 'names-numbers', 'drawing' requires Fancy Product Designer Plus Add-On.
		*
		* @property mainBarModules
		* @for Options.defaults
		* @type {Array}
		* @default ['products', 'images', 'text', 'designs']
		*/
		mainBarModules: ['products', 'images', 'text', 'designs', 'manage-layers'],
		/**
		* Set the initial active module.
		*
		* @property initialActiveModule
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		initialActiveModule: '',
		/**
		* An object defining the maximum values for input elements in the toolbar.
		*
		* @property maxValues
		* @for Options.defaults
		* @type {String}
		* @default {}
		*/
		maxValues: {},
		/**
		* Set a watermark image when the user downloads/prints the product via the actions. To pass a watermark, just enter a string with an image URL.
		*
		* @property watermark
		* @for Options.defaults
		* @type {Boolean | String}
		* @default false
		*/
		watermark: false,
		/**
		* The number of columns used for the grid images in the images and designs module.
		*
		* @property gridColumns
		* @for Options.defaults
		* @type {Number}
		* @default 2
		*/
		gridColumns: 2,
		/**
		* An object containing the currency string(use %d as placeholder for price), decimal separator and thousand separator.
		*
		* @property priceFormat
		* @for Options.defaults
		* @type {Object}
		* @default {currency: '&#36;%d', decimalSep: '.', thousandSep: ','}
		*/
		priceFormat: {currency: '&#36;%d', decimalSep: '.', thousandSep: ','},
		/**
		* The ID of an element that will be used as container for the main bar.
		*
		* @property mainBarContainer
		* @for Options.defaults
		* @type {Boolean | String}
		* @default false
		* @example #customMainBarContainer
		*/
		mainBarContainer: false,
		/**
		* The ID of an element that will be used to open the modal, in which the designer is included.
		*
		* @property modalMode
		* @for Options.defaults
		* @type {Boolean | String}
		* @default false
		* @example #modalButton
		*/
		modalMode: false,
		/**
		* Enable keyboard control. Use arrow keys to move and backspace key to delete selected element.
		*
		* @property keyboardControl
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		keyboardControl: true,
		/**
		* Deselect active element when clicking outside of the product designer.
		*
		* @property deselectActiveOnOutside
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		deselectActiveOnOutside: true,
		/**
		* All upload zones will be always on top of all elements.
		*
		* @property uploadZonesTopped
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		uploadZonesTopped: true,
		/**
		* Loads the first initial product into stage.
		*
		* @property loadFirstProductInStage
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		loadFirstProductInStage: true,
		/**
		* If the user leaves the page without saving the product or the getProduct() method is not, a alert window will pop up.
		*
		* @property unsavedProductAlert
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		unsavedProductAlert: false,
		/**
		* If the user adds something and off-canvas panel or dialog is opened, it will be closed.
		*
		* @property hideDialogOnAdd
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		hideDialogOnAdd: true,
		/**
		* Set the placement of the toolbar. For smartphones the toolbar will be fixed at the bottom of the page. Possible values:'smart', 'inside-bottom', 'inside-top'
		*
		* @property toolbarPlacement
		* @for Options.defaults
		* @type {String}
		* @default 'smart'
		*/
		toolbarPlacement: 'smart',
		/**
		* The grid size for snap action. First value defines the width on the a-axis, the second on the y-axis.
		*
		* @property snapGridSize
		* @for Options.defaults
		* @type {Array}
		* @default [50, 50]
		*/
		snapGridSize: [50, 50],
		/**
		* An object containing <a href="http://fabricjs.com/docs/fabric.Canvas.html" target="_blank">options for the fabricjs canvas</a>.
		*
		* @property fabricCanvasOptions
		* @for Options.defaults
		* @type {Object}
		* @default {}
		*/
		fabricCanvasOptions: {},
		/**
		* Defines the values for the select element in the names & numbers module. Requires Fancy Product Designer Plus Add-On.
		*
		* @property namesNumbersDropdown
		* @for Options.defaults
		* @type {Array}
		* @default []
		*/
		namesNumbersDropdown: [],
		/**
		* Sets price for any extra entry in the names & numbers module. Requires Fancy Product Designer Plus Add-On.
		*
		* @property namesNumbersEntryPrice
		* @for Options.defaults
		* @type {Number}
		* @default 0
		*/
		namesNumbersEntryPrice: 0,
		/**
		* Sets the placement for the color selection, possible values: 'inside-tl', 'inside-tc', 'inside-tr', 'inside-bl', 'inside-bc', 'inside-br' or ID of another element(#my-color-selection). Requires Fancy Product Designer Plus Add-On.
		*
		* @property colorSelectionPlacement
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		colorSelectionPlacement: '',
		/**
		* Sets the display type for the color selection. By default the color items will be shown in a grid. You can also enable a dropdown for the color selection, but this is only working when using a custom element in colorSelectionPlacement. Possible values: grid, dropdown. Requires Fancy Product Designer Plus Add-On.
		*
		* @property colorSelectionDisplayType
		* @for Options.defaults
		* @type {String}
		* @default 'grid'
		* @version PLUS 1.1.1
		*/
		colorSelectionDisplayType: 'grid',
		/**
		* Sets the placement for the Bulk-Add Variations Form. Just enter ID or class of another element(#my-color-selection). Requires Fancy Product Designer Plus Add-On.
		*
		* @property bulkVariationsPlacement
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		bulkVariationsPlacement: '',
		/**
		* The available variations for the Bulk-Add Variations Form, e.g.: {'Size': ['XL', 'L', 'M', 'S'], 'Color': ['Red', 'Blue']}. Requires Fancy Product Designer Plus Add-On.
		*
		* @property bulkVariations
		* @for Options.defaults
		* @type {Object}
		* @default {}
		*/
		bulkVariations: {},
		/**
		* The element where the toolbar will be appended when toolbarPlacement='smart'.
		*
		* @property toolbarDynamicContext
		* @for Options.defaults
		* @type {String}
		* @default 'body'
		*/
		toolbarDynamicContext: 'body',
		/**
		* Addtional properties for the bounding box. Can be used to set the stroke width etc.. See http://fabricjs.com/docs/fabric.Rect.html
		*
		* @property boundingBoxProps
		* @for Options.defaults
		* @type {Object}
		* @default {strokeWidth: 1}
		*/
		boundingBoxProps: {strokeWidth: 1},
		/**
		* If the image (custom uploaded or design) is larger than the canvas, it will be scaled down to fit into the canvas.
		*
		* @property fitImagesInCanvas
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		fitImagesInCanvas: false,
		/**
		* Set a maximum price for all products or for specific views. -1 disables the max. price.
		*
		* @property maxPrice
		* @for Options.defaults
		* @type {Number}
		* @default -1
		*/
		maxPrice: -1,
		/**
		* The text can be edited in the canvas by double click/tap.
		*
		* @property inCanvasTextEditing
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		inCanvasTextEditing: true,
		/**
		* The text input in the toolbar when be opened when an editable text is selected.
		*
		* @property openTextInputOnSelect
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		openTextInputOnSelect: false,
		/**
		* An array of design category titles (only top-level categories) to enable particular design categories for an upload zone or for the view. An empty array will enable all design categories.
		*
		* @property designCategories
		* @type {Array}
		* @for Options.defaults
		* @default []
		*/
		designCategories: [],
		/**
		* Will make the view(s) optional, so the user have to unlock it. The price for the elements in the view will be added to the total product price as soon as the view is unlocked.
		*
		* @property optionalView
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		optionalView: false,
		/**
		* When using the save/load actions, store the product in user's browser storage.
		*
		* @property saveActionBrowserStorage
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		*/
		saveActionBrowserStorage: true,
		/**
		* An array containing the pricing rules groups. Use the <a href="http://fancyproductdesigner.com/addon-pricing-rules/" target="_blank" style="text-decoration: underline;">online tool to generate pricing rules</a>. Requires Fancy Product Designer Pricing Add-On.
		*
		* @property pricingRules
		* @for Options.defaults
		* @type {Array}
		* @default []
		*/
		pricingRules: [],
		/**
		* Enables an agreement modal that needs to be confirmed before uploaded images can be used in the product designer. The text in the agreement modal can be set through the language JSON.
		*
		* @property uploadAgreementModal
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		uploadAgreementModal: false,
		/**
		* An object containing the settings for the image editor.
		*
		* @property imageEditorSettings
		* @for Options.defaults
		* @type {Object}
		* @default {masks: []}
		*/
		imageEditorSettings: {
			/**
			* An array containing the SVG urls for custom mask shapes. Use only one path per SVG, only the first path will be used as mask shape.
			*
			* @property masks
			* @type {Array}
			* @for Options.defaults.imageEditorSettings
			* @default []
			*/
			masks: []
		},
		/**
		* An object containing left, top, width and height properties that represents a printing box. A printing box is a rectangle which is always visible in the canvas and represents the printing area. It is used in the ADMIN solution to create a PDF with a specific printing area.
		*
		* @propert printingBox
		* @for Options.defaults
		* @type {Object}
		* @default null
		*/
		printingBox: null,
		/**
		* Open the Info modal when product designer is loaded. The Info action needs to be added to show the modal.
		*
		* @property autoOpenInfo
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		autoOpenInfo: false,
		/**
		* Create a custom guided tour by definifing an object with a key/css selector for the target element and the value for the text in the guided tour step. The first part of the key string defines the target type (module or action) followed by a a colon and the name of the module/action or just enter a custom CSS selector string, e.g. module:products, action:manage-layers or #any-element.
		*
		* @property guidedTour
		* @for Options.defaults
		* @type {Null | Object}
		* @default null
		* @example guidedTour: {
"module:products": "This is the text for first step.",
"action:manage-layers": "This is the text for second step.",
"#any-element": "Pointer on a custom HTML element"
}
		*/
		guidedTour: null,
		/**
		* As soon as an element with a color link group is added, the colours of this element will be used for the color group. If false, the colours of all element in the color group will be concatenated.
		*
		* @property replaceColorsInColorGroup
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		replaceColorsInColorGroup: false,
		/**
		* Defines the image types in lowercase that can be uploaded. Currently the designer supports jpg, svg, png images and PDF files.
		*
		* @property allowedImageTypes
		* @for Options.defaults
		* @type {Array}
		* @default ['jpeg', 'png', 'svg', 'pdf']
		*/
		allowedImageTypes: ['jpeg', 'png', 'svg', 'pdf'],
		/**
		* To add photos from Pixabay, you have to set an <a href="https://pixabay.com/api/docs/" target="_blank">Pixabay API key</a>.
		*
		* @property pixabayApiKey
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		pixabayApiKey: '',
		/**
		* If you want to access high-resolution images, enable this option and you have to ask Pixabay for permission. <a href="https://pixabay.com/api/docs/#hires_image_search_response" target="_blank">You can easily do that here, next to the headline</a>.
		*
		* @property pixabayHighResImages
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		pixabayHighResImages: false,
		/**
		* Language code of the language to be searched in. Accepted values: cs, da, de, en, es, fr, id, it, hu, nl, no, pl, pt, ro, sk, fi, sv, tr, vi, th, bg, ru, el, ja, ko, zh.
		*
		* @property pixabayLang
		* @for Options.defaults
		* @type {String}
		* @default ''
		* @version 4.7.5
		*/
		pixabayLang: 'en',
		/**
		* Display the internal modals (info, qr-code etc.) in the product designer instead in the whole page.
		*
		* @property openModalInDesigner
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		openModalInDesigner: false,
		/**
		* Shows the current image size in pixels in a tooltip above the image element when its selected.
		*
		* @property imageSizeTooltip
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		*/
		imageSizeTooltip: false,
		/**
		* To add photos from DepositPhotos, you have to set an <a href="https://pixabay.com/api/docs/" target="_blank">Pixabay API key</a>.
		*
		* @property depositphotosApiKey
		* @for Options.defaults
		* @type {String}
		* @default ''
		*/
		depositphotosApiKey: '',
		/**
		* The language shortcut that defines the language for the category titles. Available language shortcuts: en,de,fr,sp,ru,it,pt,es,pl,nl,jp,cz,se,zh,tr,mx,gr,ko,br,hu,uk,ro,id,th.
		*
		* @property depositphotosLang
		* @for Options.defaults
		* @type {String}
		* @default 'en'
		*/
		depositphotosLang: 'en',
		/**
		* The price that is charged when adding an image from depositphotos.com.
		*
		* @property depositphotosPrice
		* @for Options.defaults
		* @type {Number}
		* @default 0
		*/
		depositphotosPrice: 0,
		/**
		* Highlight objects (editable texts and upload zones) with a dashed border. To enable this just define a hexadecimal color value.
		*
		* @property highlightEditableObjects
		* @for Options.defaults
		* @type {String}
		* @default ''
		* @version 3.7.2
		*/
		highlightEditableObjects: '',
		/**
		* When an element is replaced, apply fill(color) from replaced element to added element.
		*
		* @property applyFillWhenReplacing
		* @for Options.defaults
		* @type {Boolean}
		* @default true
		* @version 3.7.2
		*/
		applyFillWhenReplacing: true,
		/**
		* An array containing layouts. A layout is technically a view that will replace all elements in a view when selected.
		*
		* @property layouts
		* @for Options.defaults
		* @type {Array}
		* @default []
		* @version 4.7.0
		*/
		layouts: [],
		/**
		* Options for the Dynamic Views modul. Requires Fancy Product Designer Plus Add-On.
		*
		* @property dynamicViewsOptions
		* @for Options.defaults
		* @type {Object}
		* @default {}
		* @version 4.7.0
		*/
		dynamicViewsOptions: {
			/**
			* Set the length unit that you would like to set the canvas site: 'mm', 'cm', 'inch'
			*
			* @property unit
			* @type {String}
			* @for Options.defaults.dynamicViewsOptions
			* @default 'mm'
			*/
			unit: 'mm',
			/**
			* An array will all available formats when adding a new view.
			*
			* @property formats
			* @type {Array}
			* @for Options.defaults.dynamicViewsOptions
			* @default []
			*@example [
	[100, 100],
	[500, 500],
	[1000, 1000]
]
			*/
			formats: [],
			/**
			* Charge price per area in centimeter. For example if you want to charge a price of 1 per 10cm2, you have to enter 0.1.
			*
			* @property pricePerArea
			* @type {Number}
			* @for Options.defaults.dynamicViewsOptions
			* @default 0
			*/
			pricePerArea: 0,
			/**
			* Minimum width that the user can enter as view width.
			*
			* @property minWidth
			* @type {Number}
			* @for Options.defaults.dynamicViewsOptions
			* @default 0
			*/
			minWidth: 0,
			/**
			* Minimum height that the user can enter as view height.
			*
			* @property minHeight
			* @type {Number}
			* @for Options.defaults.dynamicViewsOptions
			* @default 0
			*/
			minHeight: 0,
			/**
			* Maximum width that the user can enter as view width.
			*
			* @property maxWidth
			* @type {Number}
			* @for Options.defaults.dynamicViewsOptions
			* @default 10000
			*/
			maxWidth: 10000,
			/**
			* Maximum height that the user can enter as view height.
			*
			* @property maxHeight
			* @type {Number}
			* @for Options.defaults.dynamicViewsOptions
			* @default 10000
			*/
			maxHeight: 10000
		},
		/**
		* Emojis in text elements will be removed when changing or adding text.
		*
		* @property disableTextEmojis
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 4.7.4
		*/
		disableTextEmojis: false,
		/**
		* Enable guide lines to align the selected object to the edges of the other objects.
		*
		* @property smartGuides
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 4.7.7
		*/
		smartGuides: false,
		/**
		* Set the toolbar theme. Possible values: white, dark.
		*
		* @property toolbarTheme
		* @for Options.defaults
		* @type {String}
		* @default 'white'
		* @version 4.7.7
		*/
		toolbarTheme: 'white',
		/**
		* If a printing box has been defined for a view and the element has no individual bounding box, the printing box will be used as bounding box.
		*
		* @property usePrintingBoxAsBounding
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 4.8.0
		*/
		usePrintingBoxAsBounding: false,
		/**
		* An object defining the printing area when exporting the product as SVG. {top: Number, left: Number, width: Number, height: Number, visibility: Boolean}. The visibility property shows the printing box to the customers.
		*
		* @property printingBox
		* @for Options.defaults
		* @type {Object}
		* @default {}
		* @version 4.7.0
		* @example {top: 100, left: 100, width: 400, height: 500, visibility: true}
		*/
		printingBox: {},
		/**
		* A JSON object or URL to a JSON file that stores all initial products. These products will be displayed in the Products module.
		*
		* @property productsJSON
		* @for Options.defaults
		* @type {String}
		* @default null
		* @version 4.9.0
		*/
		productsJSON: null,
		/**
		* A JSON object or URL to a JSON file that stores all designs. These designs will be displayed in the Designs module.
		*
		* @property designsJSON
		* @for Options.defaults
		* @type {String}
		* @default null
		* @version 4.9.0
		*/
		designsJSON: null,
		/**
		* When the customizationRequired argument in the getProduct is set to true, you can control if any view needs to be customized or all. Possible values: any, all.
		*
		* @property customizationRequiredRule
		* @for Options.defaults
		* @type {String}
		* @default 'any'
		* @version 4.9.4
		*/
		customizationRequiredRule: 'any',
		/**
		* Display the notification that the product is going to be changed when clicking on a product item in the Products module.
		*
		* @property swapProductConfirmation
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 4.9.5
		*/
		swapProductConfirmation: false,
		/**
		* The position of the textarea in the toolbar. Possible values: sub, top (Only possible when toolbarPlacement = smart).
		*
		* @property toolbarTextareaPosition
		* @for Options.defaults
		* @type {String}
		* @default 'sub'
		* @version 4.9.6
		*/
		toolbarTextareaPosition: 'sub',
		/**
		* The width of a textbox can set via the "Texts" module or changed via the corner controls of the selected textbox.
		*
		* @property setTextboxWidth
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 5.0.1
		*/
		setTextboxWidth: false,
		/**
		* Define additional properties that will be applied to all text elements in the same textLinkGroup. E.g.: ['fontFamily', 'fontSize', 'fontStyle']
		*
		* @property textLinkGroupProps
		* @for Options.defaults
		* @type {Array}
		* @default []
		* @version 5.0.3
		*/
		textLinkGroupProps: [],
		/**
		* Text Templates that will appear in the Text module.
		*
		* @property textTemplates
		* @for Options.defaults
		* @type {Array}
		* @default []
		* @example [{text: 'Hello World', properties: {fontFamily: 'Arial', textSize: 35}}]
		* @version 5.1.0
		*/
		textTemplates: [],
		/**
		* Multiple objects can be selected and moved at the same time.
		*
		* @property multiSelection
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 5.1.0
		*/
		multiSelection: false,
		/**
		* The UI theme that you would like to use. Choose between 'flat' or 'doyle'.
		*
		* @property uiTheme
		* @for Options.defaults
		* @type {String}
		* @default 'flat'
		* @version 5.1.0
		*/
		uiTheme: 'flat',
		/**
		* The maximum canvas height related to the window height. A number between 0 and 1, e.g. 0.8 will set a maximum canvas height of 80% of the window height. A value of 1 will disable a calculation of a max. height.
		*
		* @property maxCanvasHeight
		* @for Options.defaults
		* @type {Number}
		* @default 1
		* @version 5.1.1
		*/
		maxCanvasHeight: 1,
		/**
		* Set the behaviour for mobile gestures. Possible values:  <ul><li>'none': No behaviour</li><li>'pinchPanCanvas': Zoom in/out and pan canvas</li><li> 'pinchImageScale': Scale selected image with pinch</li></ul> .
		*
		* @property mobileGesturesBehaviour
		* @for Options.defaults
		* @type {String}
		* @default 'none'
		* @version 5.1.3
		*/
		mobileGesturesBehaviour: 'none',
		/**
		* Enable image quality ratings for uploaded images. Therefore you can define low, mid and high quality steps. The object receives low, mid and high keys. The values of these keys are arrays, where the first entry defines the width and the second entry defines the height.
		*
		* @property imageQualityRatings
		* @for Options.defaults
		* @type {Object}
		* @default null
		* @example {low: [100, 200], mid: [500, 600], high: [1000, 1200]}
		* @version 5.1.4
		*/
		imageQualityRatings: null,
		/**
		* Displays the paths of a SVG in the advanced image editor.
		*
		* @property splitMultiSVG
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 5.1.4
		*/
		splitMultiSVG: false,
		/**
		* Set corner controls style: Basic (Rescale and Rotate), Advanced (Rescale, Rotate, Delete, Duplicate).
		*
		* @property cornerControlsStyle
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 5.1.4
		*/
		cornerControlsStyle: 'advanced',
		/**
		* The filename when the user downloads the product design as image or PDF.
		*
		* @property downloadFilename
		* @for Options.defaults
		* @type {String}
		* @default 'Product'
		* @version 5.1.5
		*/
		downloadFilename: 'Product',
		/**
		* Fill all upload zones with the first uploaded images.
		*
		* @property autoFillUploadZones
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 5.2.7
		*/
		autoFillUploadZones: false,
		/**
		* Drag & Drop images from the images and designs module into upload zones or on canvas.
		*
		* @property dragDropImagesToUploadZones
		* @for Options.defaults
		* @type {Boolean}
		* @default false
		* @version 5.2.7
		*/
		dragDropImagesToUploadZones: false,
		/**
		* An object containing the default element parameters in addition to the <a href="http://fabricjs.com/docs/fabric.Object.html" target="_blank">default Fabric Object properties</a>. See <a href="./Options.defaults.elementParameters.html">Options.defaults.elementParameters</a>.
		*
		* @property elementParameters
		* @for Options.defaults
		* @type {Object}
		*/
		elementParameters: {
			objectCaching: false,
			/**
			* Allows to set the z-index of an element, -1 means it will be added on the stack of layers
			*
			* @property z
			* @type {Number}
			* @for Options.defaults.elementParameters
			* @default -1
			*/
			z: -1,
			/**
			* The price for the element.
			*
			* @property price
			* @type {Number}
			* @for Options.defaults.elementParameters
			* @default 0
			*/
			price: 0, //how much does the element cost
			/**
			* <ul><li>If false, no colorization for the element is possible.</li><li>One hexadecimal color will enable the colorpicker</li><li>Mulitple hexadecimal colors separated by commmas will show a range of colors the user can choose from.</li></ul>
			*
			* @property colors
			* @type {Boolean | String}
			* @for Options.defaults.elementParameters
			* @default false
			* @example colors: "#000000" => Colorpicker, colors: "#000000,#ffffff" => Range of colors
			*/
			colors: false,
			/**
			* If true the user can remove the element.
			*
			* @property removable
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			removable: false,
			/**
			* If true the user can drag the element.
			*
			* @property draggable
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			draggable: false,
			/**
			* If true the user can rotate the element.
			*
			* @property rotatable
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			rotatable: false,
			/**
			* If true the user can resize the element.
			*
			* @property resizable
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			resizable: false,
			/**
			* If true the user can copy non-initial elements. Copyable property is enabled for designs and custom added elements automatically.
			*
			* @property copyable
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			copyable: false,
			/**
			* If true the user can change the z-position the element.
			*
			* @property zChangeable
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			zChangeable: false,
			/**
			* Defines a bounding box (printing area) for the element.<ul>If false no bounding box</li><li>The title of an element in the same view, then the boundary of the target element will be used as bounding box.</li><li>An object with x,y,width and height defines the bounding box</li></ul>
			*
			* @property boundingBox
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			boundingBox: false,
			/**
			* Set the mode for the bounding box. Possible values: 'none', 'clipping', 'limitModify', 'inside'
			*
			* @property boundingBoxMode
			* @type {String}
			* @for Options.defaults.elementParameters
			* @default 'clipping'
			*/
			boundingBoxMode: 'clipping',
			/**
			* Centers the element in the canvas or when it has a bounding box in the bounding box.
			*
			* @property autoCenter
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			autoCenter: false,
			/**
			* Replaces an element with the same type and replace value.
			*
			* @property replace
			* @type {String}
			* @for Options.defaults.elementParameters
			* @default ''
			*/
			replace: '',
			/**
			* If a replace value is set, you can decide if the element replaces the elements with the same replace value in all views or only in the current showing view.
			*
			* @property replaceInAllViews
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default ''
			*/
			replaceInAllViews: false,
			/**
			* Selects the element when its added to stage.
			*
			* @property autoSelect
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			autoSelect: false,
			/**
			* Sets the element always on top.
			*
			* @property topped
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			topped: false,
			/**
			* You can define different prices when using a range of colors, set through the colors option.
			*
			* @property colorPrices
			* @type {Object}
			* @for Options.defaults.elementParameters
			* @default {}
			* @example colorPrices: {"000000": 2, "ffffff: "3.5"}
			*/
			colorPrices: {},
			/**
			* Include the element in a color link group. So elements with the same color link group are changing to same color as soon as one element in the group is changing the color.
			*
			* @property colorLinkGroup
			* @type {Boolean | String}
			* @for Options.defaults.elementParameters
			* @default false
			* @example 'my-color-group'
			*/
			colorLinkGroup: false,
			/**
			* An array of URLs to pattern image - onyl for SVG images or text elements.
			*
			* @property patterns
			* @type {Array}
			* @for Options.defaults.elementParameters
			* @default []
			* @example patterns: ['patterns/pattern_1.png', 'patterns/pattern_2.png',]
			*/
			patterns: [],
			/**
			* An unique identifier for the element.
			*
			* @property sku
			* @type {String}
			* @for Options.defaults.elementParameters
			* @default ''
			*/
			sku: '',
			/**
			* When true the element is not exported in SVG. If you are going to use one of the data URL methods (e.g. <a href="./FancyProductDesigner.html#method_getProductDataURL">getProductDataURL()</a>), you need to set onlyExportable=true in the options, so the element is not exported in the data URL.
			*
			* @property excludeFromExport
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			excludeFromExport: false,
			/**
			* Shows the element colors in color selection panel. Requires Fancy Product Designer Plus Add-On.
			*
			* @property showInColorSelection
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			showInColorSelection: false,
			/**
			* By the default the element will be locked and needs to be unlocked by the user via the "Manage Layers" module.
			*
			* @property locked
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			locked: false,
			/**
			* Allow user to unlock proportional scaling in the toolbar. After that the user scale the element unproportional via toolbar or element boundary controls.
			*
			* @property uniScalingUnlockable
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			uniScalingUnlockable: false,
			/**
			* The layer is fixed and will stay on the canvas when changing the product.
			*
			* @property fixed
			* @type {Boolean}
			* @for Options.defaults.elementParameters
			* @default false
			*/
			fixed: false,
			originX: 'center',
			originY: 'center',
			cornerSize: 24,
			fill: false,
			lockUniScaling: true,
			pattern: false,
			top: 0,
			left: 0,
			angle: 0,
			flipX: false,
			flipY: false,
			opacity: 1,
			scaleX: 1,
			scaleY: 1,
		},
		/**
		* An object containing the default text element parameters in addition to the <a href="http://fabricjs.com/docs/fabric.IText.html" target="_blank">default Fabric IText properties</a>. See <a href="./Options.defaults.textParameters.html">Options.defaults.textParameters</a>. The properties in the object will merge with the properties in the elementParameters.
		*
		* @property textParameters
		* @for Options.defaults
		* @type {Object}
		*/
		textParameters: {
			/**
			* The maximal allowed characters. 0 means unlimited characters.
			*
			* @property maxLength
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 0
			*/
			maxLength: 0,
			/**
			* If true the text will be curved.
			*
			* @property curved
			* @type {Boolean}
			* @for Options.defaults.textParameters
			* @default false
			*/
			curved: false,
			/**
			* If true the the user can switch between curved and normal text.
			*
			* @property curvable
			* @type {Boolean}
			* @for Options.defaults.textParameters
			* @default false
			*/
			curvable: false,
			/**
			* The letter spacing when the text is curved.
			*
			* @property curveSpacing
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 10
			*/
			curveSpacing: 10,
			/**
			* The radius when the text is curved.
			*
			* @property curveRadius
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 80
			*/
			curveRadius: 80,
			/**
			* Reverses the curved text.
			*
			* @property curveReverse
			* @type {Boolean}
			* @for Options.defaults.textParameters
			* @default false
			*/
			curveReverse: false,
			/**
			* The maximal allowed lines. 0 means unlimited characters.
			*
			* @property maxLines
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 0
			*/
			maxLines: 0,
			/**
			* Enables the text element as a text box. A text box has a fixed width and not be resized.
			*
			* @property textBox
			* @type {Boolean}
			* @for Options.defaults.textParameters
			* @default false
			*/
			textBox: false,
			/**
			* Enables the text element as a placeholder for the Names & Numbers module. You can enable this parameter for one text element in a view.
			*
			* @property textPlaceholder
			* @type {Boolean | Array}
			* @for Options.defaults.textParameters
			* @default false
			*/
			textPlaceholder: false,
			/**
			* Enables the text element as a number placeholder for the Names & Numbers module. You can enable this parameter for one text element in a view. If you want to define a range of allowed numbers, just use an array. The first value in the array defines the minimum value, the second value defines the maximum value, e.g. [0, 10].
			*
			* @property numberPlaceholder
			* @type {Boolean}
			* @for Options.defaults.textParameters
			* @default false
			*/
			numberPlaceholder: false,
			/**
			* Addtional space between letters.
			*
			* @property letterSpacing
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 0
			*/
			letterSpacing: 0,
			/**
			* The price will be charged first after the text has been edited.
			*
			* @property chargeAfterEditing
			* @type {Boolean}
			* @for Options.defaults.textParameters
			* @default false
			*/
			chargeAfterEditing: false,
			/**
			* The minimum font size.
			*
			* @property minFontSize
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 1
			*/
			minFontSize: 1,
			/**
			* Set the text transform - none, lowercase, uppercase.
			*
			* @property textTransform
			* @type {String}
			* @for Options.defaults.textParameters
			* @default 'none'
			*/
			textTransform: 'none',
			/**
			* Set a width for the text, so the text will be scaled up/down to the given area.
			*
			* @property widthFontSize
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 0
			*/
			widthFontSize: 0,
			/**
			* The maximum font size. Using a value higher than 200 can cause performance issues with text boxes.
			*
			* @property maxFontSize
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 1
			*/
			maxFontSize: 200,
			/**
			* The color of the shadow.
			*
			* @property shadowColor
			* @type {String}
			* @for Options.defaults.textParameters
			* @default ''
			*/
			shadowColor: '',
			/**
			* Shadow Blur.
			*
			* @property shadowBlur
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 0
			*/
			shadowBlur: 0,
			/**
			* Shadow horizontal offset.
			*
			* @property shadowOffsetX
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 0
			*/
			shadowOffsetX: 0,
			/**
			* Shadow vertical offset.
			*
			* @property shadowOffsetY
			* @type {Number}
			* @for Options.defaults.textParameters
			* @default 0
			*/
			shadowOffsetY: 0,
			/**
			* Link the text of different text elements, changing the text of one element will also change the text of text elements with the same textLinkGroup value.
			*
			* @property textLinkGroup
			* @type {String}
			* @for Options.defaults.textParameters
			* @default ""
			*/
			textLinkGroup: "",
			/**
			* The colors for the stroke. If empty, the color wheel will be displayed.
			*
			* @property strokeColors
			* @type {Array}
			* @for Options.defaults.textParameters
			* @default []
			*/
			strokeColors: [],
			editable: true,
			fontFamily: "Arial",
			fontSize: 18,
			lineHeight: 1,
			fontWeight: 'normal', //set the font weight - bold or normal
			fontStyle: 'normal', //'normal', 'italic'
			textDecoration: 'normal', //'normal' or 'underline'
			padding: 10,
			textAlign: 'left',
			stroke: null,
			strokeWidth: 0,
			charSpacing: 0,
		},
		/**
		* An object containing the default image element parameters in addition to the <a href="http://fabricjs.com/docs/fabric.Image.html" target="_blank">default Fabric Image properties</a>. See <a href="./Options.defaults.imageParameters.html">Options.defaults.imageParameters</a>. The properties in the object will merge with the properties in the elementParameters.
		*
		* @property imageParameters
		* @for Options.defaults
		* @type {Object}
		*/
		imageParameters: {
			/**
			* If true the image will be used as upload zone. That means the image is a clickable area in which the user can add different media types.
			*
			* @property uploadZone
			* @type {Boolean}
			* @for Options.defaults.imageParameters
			* @default false
			*/
			uploadZone: false,
			/**
			* Sets a filter on the image. Possible values: 'grayscale', 'sepia', 'sepia2' or any filter name from FPDFilters class.
			*
			* @property filter
			* @type {Boolean}
			* @for Options.defaults.imageParameters
			* @default null
			*/
			filter: null,
			/**
			* Set the scale mode when image is added into an upload zone or resizeToW/resizeToH properties are set. Possible values: 'fit', 'cover'
			*
			* @property scaleMode
			* @type {String}
			* @for Options.defaults.imageParameters
			* @default 'fit'
			*/
			scaleMode: 'fit',
			/**
			* Resizes the uploaded image to this width. 0 means it will not be resized.
			*
			* @property resizeToW
			* @type {Number}
			* @for Options.defaults.imageParameters
			* @default 0
			*/
			resizeToW: 0,
			/**
			* Resizes the uploaded image to this height. 0 means it will not be resized.
			*
			* @property resizeToH
			* @type {Number}
			* @for Options.defaults.imageParameters
			* @default 0
			*/
			resizeToH: 0,
			/**
			* Enables advanced editing, the user can crop, set filters and manipulate the color of the image. This works only for png or jpeg images. If the original image has been edited via the image editor, the original image will be replaced by a PNG with 72DPI!
			*
			* @property advancedEditing
			* @type {Boolean}
			* @for Options.defaults.imageParameters
			* @default false
			*/
			advancedEditing: false,
			/**
			* If true the upload zone can be moved by the user.
			*
			* @property uploadZoneMovable
			* @type {Boolean}
			* @for Options.defaults.imageParameters
			* @default false
			* version 4.8.2
			*/
			uploadZoneMovable: false,
			/**
			* If true the upload zone can be removed by the user.
			*
			* @property uploadZoneRemovable
			* @type {Boolean}
			* @for Options.defaults.imageParameters
			* @default false
			* version 5.0.0
			*/
			uploadZoneRemovable: false,
			padding: 0,
			minScaleLimit: 0.01
		},
		/**
		* An object containing the default parameters for custom added images. See <a href="./Options.defaults.customImageParameters.html">Options.defaults.customImageParameters</a>. The properties in the object will merge with the properties in the elementParameters and imageParameters.
		*
		* @property customImageParameters
		* @for Options.defaults
		* @type {Object}
		*/
		customImageParameters: {
			/**
			* The minimum upload size width.
			*
			* @property minW
			* @type {Number}
			* @for Options.defaults.customImageParameters
			* @default 100
			*/
			minW: 100,
			/**
			* The minimum upload size height.
			*
			* @property minH
			* @type {Number}
			* @for Options.defaults.customImageParameters
			* @default 100
			*/
			minH: 100,
			/**
			* The maximum upload size width.
			*
			* @property maxW
			* @type {Number}
			* @for Options.defaults.customImageParameters
			* @default 1500
			*/
			maxW: 1500,
			/**
			* The maximum upload size height.
			*
			* @property maxH
			* @type {Number}
			* @for Options.defaults.customImageParameters
			* @default 1500
			*/
			maxH: 1500,
			/**
			* The minimum allowed DPI for uploaded images. Works only with JPEG images.
			*
			* @property minDPI
			* @type {Number}
			* @for Options.defaults.customImageParameters
			* @default 72
			*/
			minDPI: 72,
			/**
			* The maxiumum image size in MB.
			*
			* @property maxSize
			* @type {Number}
			* @for Options.defaults.customImageParameters
			* @default 10
			*/
			maxSize: 10
		},
		/**
		* An object containing additional parameters for custom added text.The properties in the object will merge with the properties in the elementParameters and textParameters.
		*
		* @property customTextParameters
		* @for Options.defaults
		* @type {Object}
		*/
		customTextParameters: {},
		/**
		* An object containing the supported media types the user can add in the product designer.
		*
		* @property customAdds
		* @for Options.defaults
		* @type {Object}
		*/
		customAdds: {
			/**
			* If true the user can add images from the designs library.
			*
			* @property designs
			* @type {Boolean}
			* @for Options.defaults.customAdds
			* @default true
			*/
			designs: true,
			/**
			* If true the user can add an own image.
			*
			* @property uploads
			* @type {Boolean}
			* @for Options.defaults.customAdds
			* @default true
			*/
			uploads: true,
			/**
			* If true the user can add own text.
			*
			* @property texts
			* @type {Boolean}
			* @for Options.defaults.customAdds
			* @default true
			*/
			texts: true,
			/**
			* If true the user can add own drawings.
			*
			* @property drawing
			* @type {Boolean}
			* @for Options.defaults.customAdds
			* @default true
			*/
			drawing: true
		},
		/**
		* An object containing the properties (parameters) for the QR code.
		*
		* @property qrCodeProps
		* @for Options.defaults
		* @type {Object}
		*/
		qrCodeProps: {
			/**
			* @property autoCenter
			* @type {Boolean}
			* @for Options.defaults.qrCodeProps
			* @default true
			*/
			autoCenter: true,
			/**
			* @property draggable
			* @type {Boolean}
			* @for Options.defaults.qrCodeProps
			* @default true
			*/
			draggable: true,
			/**
			* @property removable
			* @type {Boolean}
			* @for Options.defaults.qrCodeProps
			* @default true
			*/
			removable: true,
			/**
			* @property resizable
			* @type {Boolean}
			* @for Options.defaults.qrCodeProps
			* @default true
			*/
			resizable: true
		},
	};

	/**
	 * Merges the default options with custom options.
	 *
	 * @method merge
	 * @for Options
	 * @param {Object} defaults The default object.
	 * @param {Object} [merge] The merged object, that will be merged into the defaults.
	 * @return {Object} The new options object.
	 */
	this.merge = function(defaults, merge) {

		typeof merge === 'undefined' ? {} : merge;

		var options = jQuery.extend({}, defaults, merge);
		options.elementParameters = jQuery.extend({}, defaults.elementParameters, options.elementParameters);
		options.textParameters = jQuery.extend({}, defaults.textParameters, options.textParameters);
		options.imageParameters = jQuery.extend({}, defaults.imageParameters, options.imageParameters);
		options.customTextParameters = jQuery.extend({}, defaults.customTextParameters, options.customTextParameters);
		options.customImageParameters = jQuery.extend({}, defaults.customImageParameters, options.customImageParameters);
		options.customAdds = jQuery.extend({}, defaults.customAdds, options.customAdds);
		options.customImageAjaxSettings = jQuery.extend({}, defaults.customImageAjaxSettings, options.customImageAjaxSettings);
		options.qrCodeProps = jQuery.extend({}, defaults.qrCodeProps, options.qrCodeProps);
		options.imageEditorSettings = jQuery.extend({}, defaults.imageEditorSettings, options.imageEditorSettings);
		options.dynamicViewsOptions = jQuery.extend({}, defaults.dynamicViewsOptions, options.dynamicViewsOptions);
		options.priceFormat = jQuery.extend({}, defaults.priceFormat, options.priceFormat);
		options.printingBox = jQuery.extend({}, defaults.printingBox, options.printingBox);

		return options;

	};

	/**
	 * Returns all element parameter keys.
	 *
	 * @method getParameterKeys
	 * @for Options
	 * @return {Array} An array containing all element parameter keys.
	 */
	this.getParameterKeys = function() {

		var elementParametersKeys = Object.keys(this.defaults.elementParameters),
			imageParametersKeys = Object.keys(this.defaults.imageParameters),
			textParametersKeys = Object.keys(this.defaults.textParameters);

		elementParametersKeys = elementParametersKeys.concat(imageParametersKeys);
		elementParametersKeys = elementParametersKeys.concat(textParametersKeys);

		return elementParametersKeys;

	};

};

/**
 * The class to create a view. A view contains the canvas. You need to call {{#crossLink "FancyProductDesignerView/setup:method"}}{{/crossLink}} to set up the canvas with all elements, after setting an instance of {{#crossLink "FancyProductDesignerView"}}{{/crossLink}}.
 *
 * @class FancyProductDesignerView
 * @constructor
 * @param {jQuery} elem - jQuery object holding the container.
 * @param {Object} view - The default options for the view.
 * @param {Function} callback - This function will be called as soon as the view and all initial elements are loaded.
 * @param {Object} fabricjsCanvasOptions - Options for the fabricjs canvas.
 */
var FancyProductDesignerView = function($productStage, view, callback, fabricCanvasOptions) {

	'use strict';

	$ = jQuery;

	fabricCanvasOptions = typeof fabricCanvasOptions === 'undefined' ? {} : fabricCanvasOptions;

	var $this = $(this),
		instance = this,
		mouseDownStage = false,
		initialElementsLoaded = false,
		tempModifiedParameters = null,
		modifiedType = null,
		limitModifyParameters = {},
		fpdOptions = new FancyProductDesignerOptions();

	var _initialize = function() {

		/**
		 * The view title.
		 *
		 * @property title
		 * @type String
		 */
		instance.title = view.title;
		/**
		 * The view thumbnail.
		 *
		 * @property thumbnail
		 * @type String
		 */
		instance.thumbnail = view.thumbnail;
		/**
		 * The view elements.
		 *
		 * @property elements
		 * @type Object
		 */
		instance.elements = [];
		/**
		 * The view options.
		 *
		 * @property options
		 * @type Object
		 */
		instance.options = view.options;
		/**
		 * The view undos.
		 *
		 * @property undos
		 * @type Array
		 * @default []
		 */
		instance.undos = [];
		/**
		 * The view redos.
		 *
		 * @property redos
		 * @type Array
		 * @default []
		 */
		instance.redos = [];
		/**
		 * The total price for the view without max. price.
		 *
		 * @property totalPrice
		 * @type Number
		 * @default 0
		 */
		instance.totalPrice = 0;
		/**
		 * The total price for the view including max. price and corrert formatting.
		 *
		 * @property truePrice
		 * @type Number
		 * @default 0
		 */
		instance.truePrice = 0;
		/**
		 * Additional price for the view.
		 *
		 * @property additionalPrice
		 * @type Number
		 * @default 0
		 */
		instance.additionalPrice = 0;
		/**
		 * The set zoom for the view.
		 *
		 * @property zoom
		 * @type Number
		 * @default 0
		 */
		instance.zoom = 1;
		/**
		 * The responsive scale.
		 *
		 * @property responsiveScale
		 * @type Number
		 * @default 1
		 */
		instance.responsiveScale = 1;
		/**
		 * The current selected element.
		 *
		 * @property currentElement
		 * @type fabric.Object
		 * @default null
		 */
		instance.currentElement = null;
		/**
		 * The current selected bounding box object.
		 *
		 * @property currentBoundingObject
		 * @type fabric.Object
		 * @default null
		 */
		instance.currentBoundingObject = null;
		/**
		 * The title of the current selected upload zone.
		 *
		 * @property currentUploadZone
		 * @type String
		 * @default null
		 */
		instance.currentUploadZone = null;
		/**
		 * An instance of fabricjs canvas class. <a href="http://fabricjs.com/docs/fabric.Canvas.html" target="_blank">It allows to interact with the fabricjs API.</a>
		 *
		 * @property stage
		 * @type fabric.Canvas
		 * @default null
		 * @deprecated since version 4.7.7, use fCanv instead
		 */
		instance.stage = null;
		/**
		 * An instance of fabricJS canvas class. <a href="http://fabricjs.com/docs/fabric.Canvas.html" target="_blank">It allows to interact with the fabricjs API.</a>
		 *
		 * @property fCanv
		 * @type fabric.Canvas
		 * @default null
		 */
		instance.fCanv = null;
		/**
		 * The properties for the mask object (url, left, top, width, height).
		 *
		 * @property mask
		 * @type Object
		 * @default null
		 */
		instance.mask = view.mask ? view.mask : null;
		/**
		 * The image object that is going to be used as mask for this view.
		 *
		 * @property maskObject
		 * @type fabric.Image
		 * @default null
		 */
		instance.maskObject = null;
		/**
		 * A fabric.Rect representing the printing box.
		 *
		 * @property printingBoxObject
		 * @type fabric.Rect
		 * @default null
		 */
		instance.printingBoxObject = null;
		/**
		 * The locked state of the view.
		 *
		 * @property locked
		 * @type Boolean
		 * @default false
		 */
		instance.locked = view.locked !== undefined ? view.locked : view.options.optionalView;
		instance.dragStage = false;
		instance.isCustomized = false;

		//PLUS
		instance.textPlaceholder = null;
		instance.numberPlaceholder = null;
		instance.names_numbers = view.names_numbers ? view.names_numbers : null;

		//replace old width option with stageWidth
		if(instance.options.width) {
			instance.options.stageWidth = instance.options.width;
			delete instance.options['width'];
		}

		//add new canvas
		$productStage.append('<canvas></canvas>');

		$this.on('elementAdd', function(evt, element){

			if(!element) {
				return;
			}

			//check for other topped elements
			_bringToppedElementsToFront();

			if(element.isCustom && !element.hasUploadZone && !element.replace) {
				element.copyable = element.originParams.copyable = true;
				instance.stage.renderAll();
			}

		});

		//create fabric stage
		var selectionColor = '#54dfe6',
			canvas = $productStage.children('canvas:last').get(0),
			canvasOptions = $.extend({}, {
				containerClass: 'fpd-view-stage fpd-hidden',
				selection: instance.options.multiSelection,
				selectionBorderColor: selectionColor,
				selectionColor: FPDUtil.convertHexToRGBA(selectionColor, 10),
				hoverCursor: 'pointer',
				controlsAboveOverlay: true,
				centeredScaling: true,
				allowTouchScrolling: true,
				preserveObjectStacking: true
			}, fabricCanvasOptions);

		instance.fCanv = instance.stage = new fabric.Canvas(canvas, canvasOptions).on({
			'object:added': function(opts) {

				var element = opts.target,
					price = element.price;

				if(instance.options.cornerControlsStyle !== 'basic') {

					element.calcCoords = element._fpdBasicCalcCoords;
					element._setCornerCoords = element._fpdBasicsetCornerCoords;
					element._getRotatedCornerCursor = element._fpdBasicgetRotatedCornerCursor;
					element._drawControl = element._fpdBasicdrawControl;

				}

				//if element is added into upload zone, use upload zone price if one is set
				if((element._addToUZ && element._addToUZ != '')) {

					var uploadZoneObj = instance.getElementByTitle(element._addToUZ);
					price = uploadZoneObj && uploadZoneObj.price ? uploadZoneObj.price : price;

				}

				if(price !== undefined &&
					price !== 0 &&
					!element.uploadZone &&
					!element._ignore &&
					(!element.chargeAfterEditing || element._isPriced)
				) {
					element.setCoords();
					instance.changePrice(price, '+');

				}

				$this.trigger('fabricObject:added', [element]);

			},
			'object:removed': function(opts) {

				var element = opts.target;

				if(element.price !== undefined && element.price !== 0 && !element.uploadZone
					&& (!element.chargeAfterEditing || element._isPriced)) {
					instance.changePrice(element.price, '-');
				}

				$this.trigger('fabricObject:removed', [element]);

			},
			'selection:created': _selectionUpdated,
			'selection:updated': _selectionUpdated
		});

		instance.stage.setDimensions({width: instance.options.stageWidth, height: instance.options.stageHeight});

		if(instance.mask) {
			instance.setMask(instance.mask);
		}

		instance.renderPrintingBox();

	};

	/**
	* modified to use with `offsetCorner` property
	* @param absolute
	* @returns {{tl: *|fabric.Point, tr: *|fabric.Point, br: *|fabric.Point, bl: *|fabric.Point}}
	*/
	var calcCoords = function (absolute) {

		var multiplyMatrices = fabric.util.multiplyTransformMatrices,
		  transformPoint = fabric.util.transformPoint,
		  degreesToRadians = fabric.util.degreesToRadians,
		  rotateMatrix = this._calcRotateMatrix(),
		  translateMatrix = this._calcTranslateMatrix(),
		  startMatrix = multiplyMatrices(translateMatrix, rotateMatrix),
		  vpt = this.getViewportTransform(),
		  finalMatrix = absolute ? startMatrix : multiplyMatrices(vpt, startMatrix),
		  dim = this._getTransformedDimensions(),
		  w = dim.x / 2,
		  h = dim.y / 2,
		  tl = transformPoint({
		    x: -w,
		    y: -h
		  }, finalMatrix),
		  tr = transformPoint({
		    x: w,
		    y: -h
		  }, finalMatrix),
		  bl = transformPoint({
		    x: -w,
		    y: h
		  }, finalMatrix),
		  br = transformPoint({
		    x: w,
		    y: h
		  }, finalMatrix);


		//FPD: modified to use with `offsetCorner` property
		if(this.offsetCorner){
		  var cornerCenterW = dim.x / 2 + this.offsetCorner,
		    cornerCenterH = dim.y / 2 + this.offsetCorner;
		  tl._corner = transformPoint({x: -cornerCenterW, y: -cornerCenterH}, finalMatrix);
		  tr._corner = transformPoint({x:   cornerCenterW, y: -cornerCenterH}, finalMatrix);
		  bl._corner = transformPoint({x: -cornerCenterW, y: cornerCenterH}, finalMatrix);
		  br._corner = transformPoint({x: cornerCenterW, y: cornerCenterH}, finalMatrix);
		}
		//FPD: end

		if (!absolute) {
		  if (this.padding) {

		    var padding = this.padding,
		      angle = degreesToRadians(this.angle),
		      cos = fabric.util.cos(angle),
		      sin = fabric.util.sin(angle),
		      cosP = cos * padding,
		      sinP = sin * padding,
		      cosPSinP = cosP + sinP,
		      cosPMinusSinP = cosP - sinP;

		    tl.x -= cosPMinusSinP;
		    tl.y -= cosPSinP;
		    tr.x += cosPSinP;
		    tr.y -= cosPMinusSinP;
		    bl.x -= cosPSinP;
		    bl.y += cosPMinusSinP;
		    br.x += cosPMinusSinP;
		    br.y += cosPSinP;

		    //FPD: modified to use with `offsetCorner` property
		    if(this.offsetCorner) {
		      tl._corner.x -= cosPMinusSinP;
		      tl._corner.y -= cosPSinP;
		      tr._corner.x += cosPSinP;
		      tr._corner.y -= cosPMinusSinP;
		      bl._corner.x -= cosPSinP;
		      bl._corner.y += cosPMinusSinP;
		      br._corner.x += cosPMinusSinP;
		      br._corner.y += cosPSinP;
		    }
		    //FPD: end
		  }

		  var ml = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2),
		    mt = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2),
		    mr = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2),
		    mb = new fabric.Point((br.x + bl.x) / 2, (br.y + bl.y) / 2),

		    //FPD: Adjust calculation for top/right position
		    mtrX = tr.x,
		    mtrY = tr.y,
		    mtr = new fabric.Point(mtrX + sin * this.rotatingPointOffset, mtrY - cos * this.rotatingPointOffset);

		  // modified to use with `offsetCorner` property
		  if(this.offsetCorner) {
		    mtr._corner = new fabric.Point(tr._corner.x, tr._corner.y);
		  }
		  //FPD: end
		}

		var coords = {
		  tl: tl,
		  tr: tr,
		  br: br,
		  bl: bl
		};

		if (!absolute) {
		  coords.ml = ml;
		  coords.mt = mt;
		  coords.mr = mr;
		  coords.mb = mb;
		  coords.mtr = mtr;
		}

		return coords;
	};


	/**
	* Sets the coordinates of the draggable boxes in the corners of
	* the image used to scale/rotate it.
	* Edited : modified to use with `offsetCorner` property
	* @private
	*/
	var _setCornerCoords = function() {
		var coords = this.oCoords,
		  newTheta = fabric.util.degreesToRadians(45 - this.angle),
		  cornerHypotenuse = this.cornerSize * 0.707106,
		  cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta),
		  sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta),
		  x, y;

		for (var point in coords) {
		  //modified to use with `offsetCorner` property
		  if(coords[point]._corner){
		    x = coords[point]._corner.x;
		    y = coords[point]._corner.y;
		  }else{
		    x = coords[point].x;
		    y = coords[point].y;
		  }


		  coords[point].corner = {
		    tl: {
		      x: x - sinHalfOffset,
		      y: y - cosHalfOffset
		    },
		    tr: {
		      x: x + cosHalfOffset,
		      y: y - sinHalfOffset
		    },
		    bl: {
		      x: x - cosHalfOffset,
		      y: y + sinHalfOffset
		    },
		    br: {
		      x: x + sinHalfOffset,
		      y: y + cosHalfOffset
		    }
		  };
		}
	};


	var _getRotatedCornerCursor = function (corner, target, e) {
	  var n = Math.round(target.angle % 360 / 45);

	  //FPD: add CursorOffset
	  var cursorOffset = {
	    mt: 0, // n
	    tr: 1, // ne
	    mr: 2, // e
	    br: 3, // se
	    mb: 4, // s
	    bl: 5, // sw
	    ml: 6, // w
	    tl: 7 // nw
	  };

	  if (n < 0) {
	    n += 8; // full circle ahead
	  }
	  n += cursorOffset[corner];
	  n %= 8;

	  //FPD: set cursor for copy and remove
	  switch (corner) {
	    case 'tl':
	      return target.copyable ? 'copy' : 'default';
	      break;
	    case 'bl':
	      return 'pointer';
	      break;
	  }
	  return this.cursorMap[n];
	};

	/**
	 * modified to use with `offsetCorner` property
	 * @param control
	 * @param ctx
	 * @param methodName
	 * @param left
	 * @param top
	 * @private
	 */
	var _drawControl = function (control, ctx, methodName, left, top) {
	  var size = this.cornerSize,
	    iconOffset = 4,
	    iconSize = size - iconOffset * 2,
	    offsetCorner = this.offsetCorner,
	    dotSize = 4,
	    icon = false;


	  if (this.isControlVisible(control)) {

	    var wh = this._calculateCurrentDimensions(),
	      width = wh.x,
	      height = wh.y;

	    if (control == 'br' || control == 'mtr' || control == 'tl' || control == 'bl' || control == 'ml' || control == 'mr' || control == 'mb' || control == 'mt') {
	      switch (control) {

	        case 'tl':
	          //copy
	          left = left -  offsetCorner;
	          top = top -  offsetCorner;
	          icon = this.__editorMode || this.copyable ? String.fromCharCode('0xe942') : false;
	          break;
	        case 'mtr':
	          // rotate
	          var rotateRight = width / 2;
	          left = left + rotateRight +  offsetCorner;
	          top = top -  offsetCorner;
	          icon = (this.__editorMode || this.rotatable) ? String.fromCharCode('0xe923') : false;
	          break;
	        case 'br':
	          // resize
	          left = left +  offsetCorner;
	          top = top +  offsetCorner;
	          icon = (this.__editorMode || this.resizable) && this.type !== 'textbox' ? String.fromCharCode('0xe922') : false;
	          break;
	        case 'bl':
	          //remove
	          left = left -  offsetCorner;
	          top = top +  offsetCorner;
	          icon = this.__editorMode || this.removable ? String.fromCharCode('0xe926') : false;
	          break;
	      }
	    }

	    this.transparentCorners || ctx.clearRect(left, top, size, size);

	    var extraLeftOffset = control == 'mt' || control == 'mb' ? 5 : 0;
	    ctx.fillStyle = this.cornerColor;

	   if (((control == 'ml' || control == 'mr') && !this.lockScalingX) || ((control == 'mt' || control == 'mb') && !this.lockScalingY)) {
		    ctx.beginPath();
		    left += dotSize * 3;
		    top += dotSize * 3;
		    ctx.arc(left, top, dotSize, 0, 2 * Math.PI);
		    ctx.fillStyle = this.cornerIconColor;
		    ctx.fill();
		  }
		  else if(icon) {
		    ctx.fillRect(left, top, size, size);
		    ctx.font = iconSize + 'px FontFPD';
		    ctx.fillStyle = this.cornerIconColor;
		    ctx.textAlign = 'left';
		    ctx.textBaseline = 'top';
		    ctx.fillText(icon, left + iconOffset + extraLeftOffset, top + iconOffset);
		  }

	  }
	};

	var _selectionUpdated = function(opts) {

		if(instance.options.multiSelection && opts.target && opts.target.type == 'activeSelection') {

			opts.target.set({
				cornerColor: instance.options.selectedColor,
		        lockScalingX: true,
		        lockScalingY: true,
		        lockRotation: true,
		        hasControls: false,
		        rotatingPointOffset: 0,
		        borderColor: instance.options.selectedColor,
				borderDashArray: [2,2],
				rotatingPointOffset: 60,
				cornerStyle: 'circle',
				cornerSize: 16,
				transparentCorners: false,
				cornerStrokeColor: '#333f48',
				borderScaleFactor: 1.5,
		    });

		    opts.target._objects.forEach(function(obj) {

			    if(!obj.draggable || obj.locked) {
					opts.target.removeWithUpdate(obj);
			    }

			    obj.set({
		        	borderColor: instance.options.selectedColor
		    	});
		    })

		}

	};

	var _afterSetup = function() {

		callback.call(callback, instance);

		initialElementsLoaded = true;

		if(instance.options.keyboardControl) {

			$(document).on('keydown', function(evt) {

				var $target = $(evt.target);

				if(instance.currentElement && !$target.is('textarea,input[type="text"],input[type="number"]')) {

					switch(evt.which) {
						case 8:
							//remove element
							if(instance.currentElement.removable && $('.fpd-image-editor-container').length == 0) {
								instance.removeElement(instance.currentElement);
							}

						break;
				        case 37: // left

					        if(instance.currentElement.draggable) {
						        instance.setElementParameters({left: instance.currentElement.left - 1});
					        }

				        break;
				        case 38: // up

				        	if(instance.currentElement.draggable) {
						        instance.setElementParameters({top: instance.currentElement.top - 1});
					        }

				        break;
				        case 39: // right

				        	if(instance.currentElement.draggable) {
						        instance.setElementParameters({left: instance.currentElement.left + 1});
					        }

				        break;
				        case 40: // down

				        	if(instance.currentElement.draggable) {
						        instance.setElementParameters({top: instance.currentElement.top + 1});
					        }

				        break;

				        default: return; //other keys
				    }

				    evt.preventDefault();

				}

			});

		}

		//attach handlers to stage
		var lastTouchX,
			lastTouchY;

		instance.stage.on({
			'after:render': function() {

				if(instance.options.highlightEditableObjects.length > 3) {

					instance.stage.contextContainer.strokeStyle = instance.options.highlightEditableObjects;
					instance.stage.forEachObject(function(obj) {

						if(obj !== instance.stage.getActiveObject() && !obj.isMoving
							&& ((FPDUtil.getType(obj.type) === 'text' && obj.editable) || obj.uploadZone)) {

							var bound = obj.getBoundingRect();
							instance.stage.contextContainer.setLineDash([5, 15]);
							instance.stage.contextContainer.strokeRect(
				                bound.left,
				                bound.top,
				                bound.width,
				                bound.height
							);

						}
						else {
							instance.stage.contextContainer.setLineDash([]);
						}

		            });

				}

			},
			'mouse:over': function(opts) {

				if(instance.currentElement && instance.currentElement.draggable && opts.target === instance.currentElement) {
					instance.stage.hoverCursor = 'move';
				}
				else {
					instance.stage.hoverCursor = 'pointer';
				}

				/**
			     * Gets fired when the mouse gets over on fabricJS canvas.
			     *
			     * @event FancyProductDesignerView#canvas:mouseOver
			     * @param {Event} event
			     * @param {String} instance - The view instance.
			     * @param {Event} opts - FabricJS event options.
			     */
				$this.trigger('canvas:mouseOver', [instance, opts]);

			},
			'mouse:out': function(opts) {

				/**
			     * Gets fired when the mouse gets over on fabricJS canvas.
			     *
			     * @event FancyProductDesignerView#canvas:mouseOut
			     * @param {Event} event
			     * @param {String} instance - The view instance.
			     * @param {Event} opts - FabricJS event options.
			     */
				$this.trigger('canvas:mouseOut', [instance, opts]);

			},
			'mouse:down': function(opts) {

				if(opts.e.touches) {
					lastTouchX = opts.e.touches[0].clientX;
					lastTouchY = opts.e.touches[0].clientY;
				}

				mouseDownStage = true;

				//fix: when editing text via textarea and doing a modification via corner controls
				if(opts.target && opts.target.__corner && typeof opts.target.exitEditing === 'function') {
					opts.target.exitEditing();
				}

				if(opts.target == undefined) {
					instance.deselectElement();
				}
				else {

					var targetCorner = opts.target.__corner;

					//remove element
					if(instance.options.cornerControlsStyle !== 'basic' && targetCorner == 'bl' && (opts.target.removable || instance.options.editorMode)) {
						instance.removeElement(opts.target);
					}
					//copy element
					else if(instance.options.cornerControlsStyle !== 'basic' && targetCorner == 'tl' && (opts.target.copyable || instance.options.editorMode) && !opts.target.hasUploadZone) {

						instance.duplicate(opts.target);

					}
					else {
						tempModifiedParameters = instance.getElementJSON();
					}


				}

				/**
			     * Gets fired when the mouse/touch gets down on fabricJS canvas.
			     *
			     * @event FancyProductDesignerView#canvas:mouseDown
			     * @param {Event} event
			     * @param {String} instance - The view instance.
			     * @param {Event} opts - FabricJS event options.
			     */
				$this.trigger('canvas:mouseDown', [instance, opts]);

			},
			'mouse:up': function(opts) {

				$productStage.siblings('.fpd-snap-line-v, .fpd-snap-line-h').hide();

				mouseDownStage = false;

				/**
			     * Gets fired when the mouse/touch gets up on fabricJS canvas.
			     *
			     * @event FancyProductDesignerView#canvas:mouseUp
			     * @param {Event} event
			     * @param {String} instance - The view instance.
			     * @param {Event} opts - FabricJS event options.
			     */
				$this.trigger('canvas:mouseUp', [instance, opts]);

			},
			'mouse:move': function(opts) {

				if(mouseDownStage && instance.dragStage) {

					//mobile fix: touch pan
					if(opts.e.touches) {
						var currentTouchX = opts.e.touches[0].clientX,
							currentTouchY = opts.e.touches[0].clientY;
					}

					instance.stage.relativePan(new fabric.Point(
						opts.e.touches ? (currentTouchX - lastTouchX) : opts.e.movementX,
						opts.e.touches ? (currentTouchY - lastTouchY) : opts.e.movementY
					));

					//mobile fix: touch pan
					if(opts.e.touches) {
						lastTouchX = currentTouchX;
						lastTouchY = currentTouchY;
					}

				}

				/**
			     * Gets fired when the mouse/touch is moving on fabricJS canvas.
			     *
			     * @event FancyProductDesignerView#canvas:mouseMove
			     * @param {Event} event
			     * @param {String} instance - The view instance.
			     * @param {Event} opts - FabricJS event options.
			     */
				$this.trigger('canvas:mouseMove', [instance, opts]);

			},
			'text:editing:entered': function(opts) {
				$this.trigger('textEditEnter', [opts.target]);
			},
			'text:changed': function(opts) {

				instance.setElementParameters({text: opts.target.text});
				$this.trigger('textChange', [opts.target]);

			},
			'text:editing:exited':  function(opts) {
				$this.trigger('textEditExit', [opts.target]);
			},
			'object:moving': function(opts) {

				modifiedType = 'moving';

				if(!opts.target.lockMovementX || !opts.target.lockMovementY) {

					_snapToGrid(opts.target);

					if(instance.options.smartGuides) {
						_smartGuides(opts.target);
					}

				}

				instance.stage.contextContainer.strokeStyle = '#990000';

				_checkContainment(opts.target);

				/**
			     * Gets fired when an element is changing via drag, resize or rotate.
			     *
			     * @event FancyProductDesignerView#elementChange
			     * @param {Event} event
			     * @param {String} modifiedType - The modified type.
			     * @param {fabric.Object} element - The fabricJS object.
			     */
				$this.trigger('elementChange', [modifiedType, opts.target]);

			},
			'object:scaling': function(opts) {

				modifiedType = 'scaling';
				_checkContainment(opts.target);

				$productStage.siblings('.fpd-snap-line-v, .fpd-snap-line-h').hide();

				$this.trigger('elementChange', [modifiedType, opts.target]);

			},
			'object:rotating': function(opts) {

				modifiedType = 'rotating';
				_checkContainment(opts.target);

				$this.trigger('elementChange', [modifiedType, opts.target]);

			},
			'object:modified': function(opts) {

				var element = opts.target;

				if(tempModifiedParameters) {

					if(!opts.target._ignore) {
						_setUndoRedo({element: element, parameters: tempModifiedParameters, interaction: 'modify'});
						tempModifiedParameters = null;
					}

				}

				if(FPDUtil.getType(element.type) === 'text' && element.type !== 'curvedText' && !element.uniScalingUnlockable) {

					var newFontSize = opts.target.fontSize * opts.target.scaleX;

					newFontSize = parseFloat(Number(newFontSize).toFixed(0));
		            element.scaleX = 1;
		            element.scaleY = 1;
		            element._clearCache();
		            element.set('fontSize', newFontSize);
		            element.fontSize = newFontSize;

				}

				if(modifiedType !== null) {

					var modifiedParameters = {};

					switch(modifiedType) {
						case 'moving':
							modifiedParameters.left = Number(element.left);
							modifiedParameters.top = Number(element.top);
						break;
						case 'scaling':
							if(FPDUtil.getType(element.type) === 'text' && element.type !== 'curvedText' && !element.uniScalingUnlockable) {
								modifiedParameters.fontSize = parseInt(element.fontSize);
							}
							else {
								modifiedParameters.scaleX = parseFloat(element.scaleX);
								modifiedParameters.scaleY = parseFloat(element.scaleY);
							}
						break;
						case 'rotating':
							modifiedParameters.angle = element.angle;
						break;
					}

					/**
				     * Gets fired when an element is modified.
				     *
				     * @event FancyProductDesignerView#elementModify
				     * @param {Event} event
				     * @param {fabric.Object} element - The fabricJS object.
				     * @param {Object} modifiedParameters - The modified parameters.
				     */
					$this.trigger('elementModify', [element, modifiedParameters]);
				}

				modifiedType = null;

			},
			'selection:updated': _elementSelect, //Fabric V2.1
			'object:selected': _elementSelect,
		});

		instance.stage.renderAll();

		//trigger price change after view has been created to get initial price
		$this.trigger('priceChange', [0, instance.truePrice]);

	};

	var _dragStage = function(x, y) {

		instance.stage.relativePan(new fabric.Point(x, y));

	};

	var _elementSelect = function(opts) {

		var selectedElement = opts.target;

		instance.deselectElement(false);

		//dont select anything when in dragging mode
		if(instance.dragStage) {
			instance.deselectElement();
			return false;
		}

		instance.currentElement = selectedElement;

		/**
	     * Gets fired as soon as an element is selected.
	     *
	     * @event FancyProductDesignerView#elementSelect
	     * @param {Event} event
	     * @param {fabric.Object} currentElement - The current selected element.
	     */
		$this.trigger('elementSelect', [selectedElement]);

		if(instance.options.cornerControlsStyle !== 'basic') {
			selectedElement.setControlVisible('tr', false);
		}

		if(!selectedElement._ignore) {

			selectedElement.set({
				borderColor: instance.options.selectedColor,
				cornerIconColor: instance.options.cornerIconColor,
				cornerColor: instance.options.cornerControlsStyle == 'basic' ? instance.options.cornerIconColor : instance.options.selectedColor,
				borderDashArray: [2,2],
				rotatingPointOffset: instance.options.cornerControlsStyle == 'basic' ? 60 : 0,
				cornerStyle: instance.options.cornerControlsStyle == 'basic' ? 'circle' : 'rect',
				cornerSize: instance.options.cornerControlsStyle == 'basic' ? 16 : 24,
				transparentCorners: instance.options.cornerControlsStyle == 'basic' ? false : true,
				cornerStrokeColor: instance.options.cornerControlsStyle == 'basic' ? instance.options.selectedColor : null,
				borderScaleFactor: 1.5,
			});
		}

		//change cursor to move when element is draggable
		selectedElement.draggable ? instance.stage.hoverCursor = 'move' : instance.stage.hoverCursor = 'pointer';

		//check for a boundingbox
		if(selectedElement.boundingBox && !selectedElement.uploadZone) {
			instance.renderElementBoundingBox(selectedElement);
		}

	};

	var _setUndoRedo = function(undo, redo, trigger) {

		trigger = typeof trigger === 'undefined' ? true : trigger;

		if(undo) {
			instance.undos.push(undo);

			if(instance.undos.length > 20) {
				instance.undos.shift();
			}
		}

		if(redo) {
			instance.redos.push(redo);
		}

		instance.isCustomized = true;

		if(trigger) {

			/**
		     * Gets fired when the canvas has been saved in the undos or redos array.
		     *
		     * @event FancyProductDesignerView#undoRedoSet
		     * @param {Event} event
		     * @param {Array} undos - An array containing all undo objects.
		     * @param {Array} redos - An array containing all redos objects.
		    */

			$this.trigger('undoRedoSet', [instance.undos, instance.redos]);

		}

	};

	//brings all topped elements to front
	var _bringToppedElementsToFront = function() {

		var objects = instance.stage.getObjects(),
			bringToFrontObj = [];

		for(var i = 0; i < objects.length; ++i) {
			var object = objects[i];
			if(object.topped || (object.uploadZone && instance.options.uploadZonesTopped)) {
				bringToFrontObj.push(object);
			}
		}

		for(var i = 0; i < bringToFrontObj.length; ++i) {
			bringToFrontObj[i].bringToFront();
		}

		//bring all elements inside a upload zone to front
		/*for(var i = 0; i < objects.length; ++i) {
			var object = objects[i];
			if(object.hasUploadZone) {
				object.bringToFront().setCoords();
			}
		}*/

		if(instance.currentBoundingObject) {
			instance.currentBoundingObject.bringToFront();
		}

		if(instance.printingBoxObject) {
			instance.printingBoxObject.bringToFront();
		}

		var snapLinesGroup = instance.getElementByID('_snap_lines_group');
		if(snapLinesGroup) {
			snapLinesGroup.bringToFront();
		}

		instance.stage.renderAll();

	};

	var _snapToGrid = function(element) {

		if(instance._snapElements) {

			var gridX = instance.options.snapGridSize[0] ? instance.options.snapGridSize[0] : 50,
				gridY = instance.options.snapGridSize[1] ? instance.options.snapGridSize[1] : 50,
				currentPosPoint = element.getPointByOrigin('left', 'top'),
				point = new fabric.Point(element.padding + (Math.round(currentPosPoint.x / gridX) * gridX), element.padding + (Math.round(currentPosPoint.y / gridY) * gridY));

				element.setPositionByOrigin(point, 'left', 'top');

		}

	};

	var _smartGuides = function(targetObj) {

		$productStage.siblings('.fpd-snap-line-v, .fpd-snap-line-h').hide();

		var allElements = instance.stage.getObjects().filter(function(t){
			return t.hasRotatingPoint;
		});

	    var bb = instance.currentBoundingObject;

	    if(!bb) {
		    bb = {
			    left: 0,
			    top: 0,
			    width: instance.options.stageWidth,
			    height: instance.options.stageHeight,
		    }
	    }

	    var point = instance.stage.gridSnapMove({
			tolerance: 8,
			guidlines: [
				{cx: bb.left+bb.width/2},
				{cy: bb.top+bb.height/2}
			],
			objects: allElements,
			target: targetObj
	    });

	    delete instance.stage.__snapCache;

	    if(point) {

			if(point.x !== undefined){
				$productStage.siblings('.fpd-snap-line-v')
				.css('left', $productStage.position().left + point.x * instance.responsiveScale ).show();
			}
			if(point.y !== undefined) {
				$productStage.siblings('.fpd-snap-line-h')
				.css('top', $productStage.position().top + (point.y * instance.responsiveScale) ).show();
			}

	    }

	};

	//checks if an element is in its containment (bounding box)
	var _checkContainment = function(target) {

		if(instance.currentBoundingObject && !target.hasUploadZone) {

			target.setCoords();

			if(target.boundingBoxMode === 'limitModify') {

				var targetBoundingRect = target.getBoundingRect(),
					bbBoundingRect = instance.currentBoundingObject.getBoundingRect(),
					minX = bbBoundingRect.left,
					maxX = bbBoundingRect.left+bbBoundingRect.width-targetBoundingRect.width,
					minY = bbBoundingRect.top,
					maxY = bbBoundingRect.top+bbBoundingRect.height-targetBoundingRect.height;

				//check if target element is not contained within bb
			    if(!target.isContainedWithinObject(instance.currentBoundingObject)) {

					//check if no corner is used, 0 means its dragged
					if(target.__corner === 0) {
						if(targetBoundingRect.left > minX && targetBoundingRect.left < maxX) {
						   limitModifyParameters.left = target.left;
					    }

					    if(targetBoundingRect.top > minY && targetBoundingRect.top < maxY) {
						   limitModifyParameters.top = target.top;
					    }
					}

			        target.setOptions(limitModifyParameters);


			    } else {

				    limitModifyParameters = {left: target.left, top: target.top, angle: target.angle, scaleX: target.scaleX, scaleY: target.scaleY};
				    if(FPDUtil.getType(target.type) == 'text') {
					    limitModifyParameters.fontSize = target.fontSize;
					     limitModifyParameters.lineHeight = target.lineHeight;
					    limitModifyParameters.charSpacing = target.charSpacing;
				    }

			    }

				/**
			     * Gets fired when the containment of an element is checked.
			     *
			     * @event FancyProductDesignerView#elementCheckContainemt
			     * @param {Event} event
			     * @param {fabric.Object} target
			     * @param {Boolean} boundingBoxMode
			     */
			    $this.trigger('elementCheckContainemt', [target, 'limitModify']);

			}
			else if(target.boundingBoxMode === 'inside' || target.boundingBoxMode === 'clipping') {

				var isOut = false,
					tempIsOut = target.isOut;

					isOut = !target.isContainedWithinObject(instance.currentBoundingObject);

				if(isOut) {

					if(target.boundingBoxMode === 'inside') {
						target.borderColor = instance.options.outOfBoundaryColor;
					}

					target.isOut = true;

				}
				else {

					if(target.boundingBoxMode === 'inside') {
						target.borderColor = instance.options.selectedColor;
					}

					target.isOut = false;

				}

				if(tempIsOut != target.isOut && tempIsOut != undefined) {
					if(isOut) {

						/**
					     * Gets fired as soon as an element is outside of its bounding box.
					     *
					     * @event FancyProductDesignerView#elementOut
					     * @param {Event} event
					     */
						$this.trigger('elementOut', [target]);
					}
					else {

						/**
					     * Gets fired as soon as an element is inside of its bounding box again.
					     *
					     * @event FancyProductDesignerView#elementIn
					     * @param {Event} event
					     */
						$this.trigger('elementIn', [target]);
					}
				}

				$this.trigger('elementCheckContainemt', [target, target.boundingBoxMode]);

			}

		}

		instance.stage.renderAll();

	};

	//center object
	var _centerObject = function(object, hCenter, vCenter) {

		var cp = object.getCenterPoint(),
			boundingBox = instance.getBoundingBoxCoords(object);

		var left = cp.x,
			top = cp.y;

		if(hCenter) {

			if(boundingBox) {
				left = boundingBox.left + boundingBox.width * 0.5;
				//left = cp.x;
			}
			else {
				left = instance.options.stageWidth * 0.5;
			}

		}

		if(vCenter) {
			if(boundingBox) {
				top = boundingBox.top + boundingBox.height * 0.5;
				//top = cp.y;
			}
			else {
				top = instance.options.stageHeight * 0.5;
			}

		}

		object.setPositionByOrigin(new fabric.Point(left, top), 'center', 'center');

		instance.stage.renderAll();
		object.setCoords();

		_checkContainment(object);

	};

	//sets the price for the element if it has color prices
	var _setColorPrice = function(element, hex) {

		//only execute when initial elements are loaded and element has color prices and colors is an object
		if(initialElementsLoaded && element.colorPrices && typeof element.colors === 'object' && element.colors.length > 1) {

			//subtract current color price, if set and is hex
			if(element.currentColorPrice !== undefined) {
				element.price -= element.currentColorPrice;
				instance.changePrice(element.currentColorPrice, '-');
			}

			if(typeof hex === 'string') {

				var hexKey = hex.replace('#', '');

				if(element.colorPrices.hasOwnProperty(hexKey) || element.colorPrices.hasOwnProperty(hexKey.toUpperCase())) {

					var elementColorPrice = element.colorPrices[hexKey] === undefined ? element.colorPrices[hexKey.toUpperCase()] : element.colorPrices[hexKey];

					element.currentColorPrice = elementColorPrice;
					element.price += element.currentColorPrice;
					instance.changePrice(element.currentColorPrice, '+');

				}
				else {
					element.currentColorPrice = 0;
				}

			}
			else {
				element.currentColorPrice = 0;
			}

		}

	};

	//sets the pattern for a svg image or text
	var _setPattern = function(element, url) {

		var _loadFromScript = instance.options._loadFromScript ? instance.options._loadFromScript : '';
		if(url) {
			url = _loadFromScript + url;
		}
		if(FPDUtil.isSVG(element)) {

			if(url) {

				fabric.util.loadImage(url, function(img) {

					if(typeof element.getObjects == 'function') { //multi-path svg
						var paths = element.getObjects();
						for(var i=0; i < paths.length; ++i) {
							paths[i].set('fill', new fabric.Pattern({
								source: img,
								repeat: 'repeat'
							}));
						}
					}
					else { //single path SVG
						element.set('fill', new fabric.Pattern({
							source: img,
							repeat: 'repeat'
						}));
					}

					instance.stage.renderAll();

				});
			}

		}
		else if(FPDUtil.getType(element.type) === 'text') {

			if(url) {
				fabric.util.loadImage(url, function(img) {

					element.set('fill', new fabric.Pattern({
						source: img,
						repeat: 'repeat'
					}));
					instance.stage.renderAll();
				});
			}
			else {
				var color = element.fill ? element.fill : element.colors[0];
				color = color ? color : '#000000';
				element.set('fill', color);
			}

		}

	};

	//defines the clipping area
	var _clipElement = function(element) {

		var bbCoords = instance.getBoundingBoxCoords(element) || element.clippingRect;
		if(bbCoords) {

			element.clippingRect = bbCoords;

			if(fabric.version.indexOf('3.') == 0) {

				var clipRect = new fabric.Rect({
					originX: 'left',
					originY: 'top',
					angle: bbCoords.angle || 0,
					left: bbCoords.left,
					top: bbCoords.top,
					width: bbCoords.width,
					height: bbCoords.height,
					fill: '#DDD',
					absolutePositioned: true,
				});

				element.clipPath = clipRect;

			}
			else {

				element.clipTo = function(ctx) {
					_clipById(ctx, this);
				};

			}

		}

	};

	//draws the clipping
	var _clipById = function (ctx, _this, scale) {

		scale = scale === undefined ? 1 : scale;

		var clipRect = _this.clippingRect;

	    ctx.save();

	    var m = _this.calcTransformMatrix(),
			iM = fabric.util.invertTransform(m);

		ctx.transform.apply(ctx, iM);
		//ctx.rotate(20 * Math.PI / 180);
		ctx.translate(0, 0);
	    ctx.beginPath();
	    ctx.rect(
	        clipRect.left,
	        clipRect.top,
	        clipRect.width * scale,
	        clipRect.height * scale
	    );
	    ctx.fillStyle = 'transparent';
	    ctx.fill();
	    ctx.closePath();
	    ctx.restore();

	};

	var _elementHasUploadZone = function(element) {

		if(element && element.hasUploadZone) {

			//check if upload zone contains objects
			var objects = instance.stage.getObjects(),
				uploadZoneEmpty = true;

			for(var i=0; i < objects.length; ++i) {

				var object = objects[i];
				if(object.replace == element.replace) {
					uploadZoneEmpty = false;
					break;
				}

			}

			var uploadZoneObject = instance.getUploadZone(element.replace);
			if(uploadZoneObject) {
				uploadZoneObject.set('opacity', uploadZoneEmpty ? 1 : 0);
				uploadZoneObject.evented = uploadZoneEmpty;
			}

			instance.stage.renderAll();
		}

	};

	var _maxTextboxLines = function(textbox, text) {

		textbox.set('text', text); //render text

		//loop: remove chars as long as lineHeights = maxLines
		while(textbox.__lineHeights.length > textbox.maxLines) {
			text = textbox.text;
			text = text.slice(0, -1);
			textbox.set('text', text);
			//if lineHeights are ok, exit editing
			if(textbox.__lineHeights.length <= textbox.maxLines) {
				textbox.exitEditing();
			}
		}

		return text;

	};

	//return an element by ID
	this.getElementByID = function(id) {

		var objects = instance.stage.getObjects();
		for(var i=0; i < objects.length; ++i) {
			if(objects[i].id == id) {
				return objects[i];
				break;
			}
		}

		return false;

	};

	/**
	 * Adds a new element to the view.
	 *
	 * @method addElement
	 * @param {string} type The type of an element you would like to add, 'image' or 'text'.
	 * @param {string} source For image the URL to the image and for text elements the default text.
	 * @param {string} title Only required for image elements.
	 * @param {object} [parameters] An object with the parameters, you would like to apply on the element.
	 */
	this.addElement = function(type, source, title, params) {

		if(type === undefined || source === undefined || title === undefined) {
			return;
		}

		/**
	     * Gets fired as soon as an element has beed added.
	     *
	     * @event FancyProductDesignerView#beforeElementAdd
	     * @param {Event} event
	     * @param {String} type - The element type.
	     * @param {String} source - URL for image, text string for text element.
	     * @param {String} title - The title for the element.
	     * @param {Object} params - The default properties.
	     */
		$this.trigger('beforeElementAdd', [type, source, title, params]);

		params = typeof params !== 'undefined' ? params : {};
		if(type === 'text') {
			//strip HTML tags
			source = source.replace(/(<([^>]+)>)/ig,"");
			title = title.replace(/(<([^>]+)>)/ig,"");
		}

		if(typeof params != "object") {
			FPDUtil.showModal("The element "+title+" does not have a valid JSON object as parameters! Please check the syntax, maybe you set quotes wrong.");
			return false;
		}

		//check that fill is a string
		if(typeof params.fill !== 'string' && !$.isArray(params.fill)) {
			params.fill = false;
		}

		//replace depraceted keys
		params = FPDUtil.rekeyDeprecatedKeys(params);

		//merge default options
		if(FPDUtil.getType(type) === 'text') {
			params = $.extend({}, instance.options.elementParameters, instance.options.textParameters, params);
		}
		else {
			params = $.extend({}, instance.options.elementParameters, instance.options.imageParameters, params);
		}

		var pushTargetObject = false,
			targetObject = null;

		//store current color and convert colors in string to array
		if(params.colors && typeof params.colors == 'string') {

			//check if string contains hex color values
			if(params.colors.indexOf('#') == 0) {
				//convert string into array
				var colors = params.colors.replace(/\s+/g, '').split(',');
				params.colors = colors;
			}

		}

		params._isInitial = !initialElementsLoaded;

		if(FPDUtil.getType(type) === 'text') {
			var defaultTextColor = params.colors[0] ? params.colors[0] : '#000000';
			params.fill = params.fill ? params.fill : defaultTextColor;
		}

		var fabricParams = {
			source: source,
			title: title,
			id: String(new Date().getTime()),
			cornerColor: instance.options.selectedColor,
			cornerIconColor: instance.options.cornerIconColor
		};

		if(!instance.options.editorMode) {

			$.extend(fabricParams, {
				selectable: false,
				lockRotation: true,
				hasRotatingPoint: false,
				lockScalingX: true,
				lockScalingY: true,
				lockMovementX: true,
				lockMovementY: true,
				hasControls: false,
				evented: false,
				lockScalingFlip: true
			});

		}
		else {
			params.__editorMode = instance.options.editorMode;
			fabricParams.selectable = fabricParams.evented = true;
		}

		fabricParams = $.extend({}, params, fabricParams);

		if(fabricParams.isCustom) {
			instance.isCustomized = true;
		}

		if(instance.options.usePrintingBoxAsBounding && !fabricParams.boundingBox && FPDUtil.objectHasKeys(instance.options.printingBox, ['left','top','width','height'])) {

			fabricParams.boundingBox = {
				x: instance.options.printingBox.left-1,
				y: instance.options.printingBox.top-1,
				width: instance.options.printingBox.width+1,
				height: instance.options.printingBox.height+1
			};
		}

		if(type == 'image' || type == 'path' || type == FPDPathGroupName) {

			fabricParams.crossOrigin = '';
			fabricParams.lockUniScaling = instance.options.editorMode ? false : !fabricParams.uniScalingUnlockable;

			//remove url parameters
			if(source.search('<svg ') === -1) {
				var splitURLParams = source.split('?');
				source = fabricParams.source = splitURLParams[0];
			}

			var _fabricImageLoaded = function(fabricImage, params, vectorImage, originParams) {

				if(fabricImage) {

					originParams = originParams === undefined ? {} : originParams;

					$.extend(params, {
						originParams: $.extend({}, params, originParams)
					});

					fabricImage.setOptions(params);
					instance.stage.add(fabricImage);
					instance.setElementParameters(params, fabricImage, false);

					fabricImage.originParams.angle = fabricImage.angle;
					fabricImage.originParams.z = instance.getZIndex(fabricImage);

					if(instance.options.improvedResizeQuality && !vectorImage) {

						fabricImage.resizeFilter = new fabric.Image.filters.Resize({type: 'hermite'});

					}

					if(!fabricImage._isInitial && !fabricImage._ignore) {

						_setUndoRedo({
							element: fabricImage,
							parameters: params,
							interaction: 'add'
						});

					}

				}
				else {

					FPDUtil.showModal("The image with the URL<br /><i style='font-size: 10px;'>"+params.source+"</i><br />can not be loaded into the canvas. <p><br />Troubleshooting<br/><ul><li>The URL is not correct!</li><li>The image has been blocked by <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS' target='_blank'>CORS policy</a>. You need to host the image under the same protocol and domain or enable 'Access-Control-Allow-Origin' on the server where you host the image. <a href='http://enable-cors.org/' target='_blank'>Read more about it here.</a></li></ul></p>");

				}

				/**
			     * Gets fired as soon as an element has beed added.
			     *
			     * @event FancyProductDesignerView#elementAdd
			     * @param {Event} event
			     * @param {fabric.Object} object - The fabric object.
			     */
				$this.trigger('elementAdd', [fabricImage]);

			};


			if(source === undefined || source.length === 0) {
				FPDUtil.log('No image source set for: '+ title);
				return;
			}

			//add SVG from XML document
			if(source.search('<svg') !== -1) {

				fabric.loadSVGFromString(source, function(objects, options) {
					var svgGroup = fabric.util.groupSVGElements(objects, options);

					//replace fill prop with svgFill
					if(fabricParams.fill) {

						if(!fabricParams.svgFill) {
							fabricParams.svgFill = fabricParams.fill;
						}

						delete fabricParams['fill'];
					}
					//if no default colors are set, use the initial path colors
					else if(!fabricParams.fill && !fabricParams.svgFill) {

						if(objects) {
							params.colors = [];
							for(var i=0; i < objects.length; ++i) {
								var color = objects[i].fill.length > 0 ? tinycolor(objects[i].fill).toHexString() : 'transparent';
								params.colors.push(color);
							}
							params.svgFill = params.colors;
						}

						fabricParams.svgFill = params.svgFill;
					}


					delete fabricParams['clippingRect'];
					delete fabricParams['boundingBox'];
					delete fabricParams['originParams'];
					delete fabricParams['colors'];
					delete fabricParams['svgFill'];
					delete fabricParams['width'];
					delete fabricParams['height'];
					delete fabricParams['originX'];
					delete fabricParams['originY'];
					delete fabricParams['objectCaching'];


					_fabricImageLoaded(svgGroup, fabricParams, true, {svgFill: params.svgFill});
				});

			}
			//load svg from url
			else if($.inArray('svg', source.split('.')) != -1) {

				var timeStamp = Date.now().toString(),
					_loadFromScript = instance.options._loadFromScript ? instance.options._loadFromScript : '',
					url = FPDUtil.isUrl(source) ? new URL(_loadFromScript + source) : source;

				//add timestamp when option enabled or is cloudfront url
				if((source.includes('.cloudfront.net/') || instance.options.imageLoadTimestamp)
					&& !instance.options._loadFromScript) {

					url.searchParams.append('t', timeStamp);

				}

				if(typeof url === 'object') {
					url = url.toString();
				}

				fabric.loadSVGFromURL(url, function(objects, options) {

					//if objects is null, svg is loaded from external server with cors disabled
					var svgGroup = objects ? fabric.util.groupSVGElements(objects, options) : null;

					//replace fill prop with svgFill
					if(fabricParams.fill) {

						if(!fabricParams.svgFill) {
							fabricParams.svgFill = fabricParams.fill;
						}

						delete fabricParams['fill'];
					}
					//if no default colors are set, use the initial path colors
					else if(!fabricParams.fill && !fabricParams.svgFill) {

						if(objects) {
							params.colors = [];
							for(var i=0; i < objects.length; ++i) {
								var color = objects[i].fill.length > 0 ? tinycolor(objects[i].fill).toHexString() : 'transparent';
								params.colors.push(color);
							}
							params.svgFill = params.colors;
						}

						fabricParams.svgFill = params.svgFill;
					}
					_fabricImageLoaded(svgGroup, fabricParams, true, {svgFill: params.svgFill});

				});

			}
			//load png/jpeg from url
			else {

				var timeStamp = Date.now().toString(),
					_loadFromScript = instance.options._loadFromScript ? instance.options._loadFromScript : '',
					url;

				if(source.indexOf('data:image/') == -1) {//do not add timestamp to data URI

					url = FPDUtil.isUrl(source) ? new URL(_loadFromScript + source) : source

					//add timestamp when option enabled or is cloudfront url
					if((source.includes('.cloudfront.net/') || instance.options.imageLoadTimestamp)
						&& !instance.options._loadFromScript) {
						url.searchParams.append('t', timeStamp);
					}

					if(typeof url === 'object') {
						url = url.toString();
					}

				}
				else {
					url = source;
				}

				new fabric.Image.fromURL(url, function(fabricImg) {

					//if src is empty, image is loaded from external server with cors disabled
					fabricImg = fabricImg.getSrc() === '' ? null : fabricImg;
					_fabricImageLoaded(fabricImg, fabricParams, false);

				}, {crossOrigin: 'anonymous'});

			}

		}
		else if(FPDUtil.getType(type) === 'text') {

			source = source.replace(/\\n/g, '\n');
			params.text = params.text ? params.text : source;
			fabricParams._initialText = params.hasOwnProperty('_initialText') ? params._initialText : params.text;

			$.extend(fabricParams, {
				spacing: params.curveSpacing,
				radius: params.curveRadius,
				reverse: params.curveReverse,
				originParams: $.extend({}, params)
			});

			//ensure origin text is always the initial text, even when action:save
			if(params.originParams && params.originParams.text) {
				fabricParams.originParams.text = fabricParams._initialText;
			}

			//make text curved
			var fabricText;
			if(params.curved && typeof fabric.CurvedText !== 'undefined') {

				var _tempText = fabricParams.text; //fix: text property gets empty, when creating curved text
				fabricText = new fabric.CurvedText(source, fabricParams);
				fabricParams.text = _tempText;

			}
			//make text box
			else if(params.textBox) {

				fabricParams.lockUniScaling = !instance.options.editorMode;

				if(instance.options.setTextboxWidth) {
					fabricParams.lockUniScaling = false;
					fabricParams.lockScalingX = false;
				}

				fabricParams.lockScalingY = true;

				fabricText = new fabric.Textbox(source, fabricParams);
				fabricText.setControlVisible('bl', true);

				if(!instance.options.inCanvasTextEditing) {
					fabricText.on({'editing:entered': function() {
						this.exitEditing();
					}});
				}

			}
			//just interactive text
			else {
				fabricText = new fabric.IText(source, fabricParams);

				if(!instance.options.inCanvasTextEditing) {
					fabricText.on({'editing:entered': function() {
						this.exitEditing();
					}});
				}

			}

			if(fabricParams.textPlaceholder || fabricParams.numberPlaceholder) {

				if(fabricParams.textPlaceholder) {
					instance.textPlaceholder = fabricText;
					fabricParams.removable = false;
					fabricParams.editable = false;
				}

				if(fabricParams.numberPlaceholder) {
					instance.numberPlaceholder = fabricText;
					fabricParams.removable = false;
					fabricParams.editable = false;
				}

			}

			instance.stage.add(fabricText);
			instance.setElementParameters(fabricParams, fabricText, false);

			fabricText.originParams = $.extend({}, fabricText.toJSON(), fabricText.originParams);
			delete fabricText.originParams['clipTo'];
			fabricText.originParams.z = instance.getZIndex(fabricText);

			if(!fabricText._isInitial && !fabricText._ignore) {
				_setUndoRedo({
					element: fabricText,
					parameters: fabricParams,
					interaction: 'add'
				});
			}

			$this.trigger('elementAdd', [fabricText]);

		}
		else {

			FPDUtil.showModal('Sorry. This type of element is not allowed!');

		}

	};

	/**
	 * Returns an fabric object by title.
	 *
	 * @method getElementByTitle
	 * @param {string} title The title of an element.
	 * @return {Object} FabricJS Object.
	 */
	this.getElementByTitle = function(title) {

		var objects = instance.stage.getObjects();
		for(var i = 0; i < objects.length; ++i) {
			if(objects[i].title === title) {
				return objects[i];
				break;
			}
		}

	};

	/**
	 * Deselects the current selected element.
	 *
	 * @method deselectElement
	 * @param {boolean} [discardActiveObject=true] Discards the active element.
	 */
	this.deselectElement = function(discardActiveObject) {

		discardActiveObject = typeof discardActiveObject == 'undefined' ? true : discardActiveObject;

		if(instance.currentBoundingObject) {

			instance.stage.remove(instance.currentBoundingObject);
			$this.trigger('boundingBoxToggle', [instance.currentBoundingObject, false]);
			instance.currentBoundingObject = null;

		}

		if(discardActiveObject) {
			instance.stage.discardActiveObject();
		}

		instance.currentElement = null;
		instance.stage.renderAll().calcOffset();

		$this.trigger('elementSelect', [null]);

	};

	/**
	 * Removes an element using the fabric object or the title of an element.
	 *
	 * @method removeElement
	 * @param {object|string} element Needs to be a fabric object or the title of an element.
	 */
	this.removeElement = function(element) {

		if(typeof element === 'string') {
			element = instance.getElementByTitle(element);
		}

		if(!element._ignore) {

			var params = instance.getElementJSON(element);
			params.z = instance.getZIndex(element);

			_setUndoRedo({
				element: element,
				parameters: params,
				interaction: 'remove'
			});

		}

		this.deselectElement();

		setTimeout(function() {

			instance.stage.remove(element);
			_elementHasUploadZone(element);

			/**
		     * Gets fired as soon as an element has been removed.
		     *
		     * @event FancyProductDesignerView#elementRemove
		     * @param {Event} event
		     * @param {fabric.Object} element - The fabric object that has been removed.
		     */
			$this.trigger('elementRemove', [element]);

		}, 1);


	};

	/**
	 * Sets the parameters for a specified element.
	 *
	 * @method setElementParameters
	 * @param {object} parameters An object with the parameters that should be applied to the element.
	 * @param {fabric.Object | string} [element] A fabric object or the title of an element. If no element is set, the parameters will be applied to the current selected element.
	 * @param {Boolean} [saveUndo=true] Save new parameters also in undos.
	 */
	this.setElementParameters = function(parameters, element, saveUndo) {

		element = typeof element === 'undefined' ? instance.stage.getActiveObject() : element;
		saveUndo = typeof saveUndo === 'undefined' ? true : saveUndo;

		if(!element || parameters === undefined) {
			return false;
		}

		//if element is string, get by title
		if(typeof element == 'string') {
			element = instance.getElementByTitle(element);
		}

		var elemType = FPDUtil.getType(element.type);

		//store undos
		if(saveUndo && initialElementsLoaded) {

			var undoParameters = instance.getElementJSON();

			if(element._tempFill) {
				undoParameters.fill = element._tempFill;
				element._tempFill = undefined;
			}

			if(!element._ignore) {
				_setUndoRedo({
					element: element,
					parameters: undoParameters,
					interaction: 'modify'
				});
			}

		}

		//scale image into bounding box (cover or fit)
		if(FPDUtil.getType(element.type) == 'image' && !element._isInitial && !element._addToUZ && element.scaleX === 1) {


			var scale = null;
			 if(!FPDUtil.isZero(element.resizeToW) || !FPDUtil.isZero(element.resizeToH)) {

				var scaleToWidth = element.resizeToW,
				 	scaleToHeight = element.resizeToH;

				scaleToWidth = isNaN(scaleToWidth) ? (parseFloat(scaleToWidth) / 100) *  instance.options.stageWidth : parseInt(scaleToWidth);
				scaleToHeight = isNaN(scaleToHeight) ? (parseFloat(scaleToHeight) / 100) * instance.options.stageHeight : parseInt(scaleToHeight);

				scale = FPDUtil.getScalingByDimesions(
					element.width,
					element.height,
					scaleToWidth,
					scaleToHeight,
					element.scaleMode
				);

			}
			//only scale to bb when no scale value is set
			else if(element.boundingBox) {

				var bb = instance.getBoundingBoxCoords(element);

				scale = FPDUtil.getScalingByDimesions(
					element.width,
					element.height,
					bb.width,
					bb.height,
					element.scaleMode
				);

			}
			else if(instance.options.fitImagesInCanvas) {

				var iconTolerance = element.cornerSize * 3;

				if((element.width * element.scaleX) + iconTolerance > instance.options.stageWidth
					|| (element.height * element.scaleY) + iconTolerance > instance.options.stageHeight) {

					scale = FPDUtil.getScalingByDimesions(
						element.width,
						element.height,
						instance.options.stageWidth - iconTolerance,
						instance.options.stageHeight - iconTolerance
					);

				}
			}

			if(scale !== null) {
				$.extend(parameters, {scaleX: scale, scaleY: scale});
			}

		}

		//adds the element into a upload zone
		if((element._addToUZ && element._addToUZ != '')) {

			parameters.z = -1;
			var uploadZoneObj = instance.getElementByTitle(element._addToUZ),
				scale = 1;

			if(FPDUtil.getType(element.type) == 'image') {
				scale = FPDUtil.getScalingByDimesions(
					element.width,
					element.height,
					uploadZoneObj.width * uploadZoneObj.scaleX,
					uploadZoneObj.height * uploadZoneObj.scaleY,
					uploadZoneObj.scaleMode
				);
			}

			$.extend(parameters, {
					boundingBox: element._addToUZ,
					boundingBoxMode: 'clipping',
					scaleX: scale,
					scaleY: scale,
					autoCenter: true,
					removable: true,
					zChangeable: false,
					autoSelect: false,
					copyable: false,
					hasUploadZone: true,
					z: instance.getZIndex(instance.getElementByTitle(element._addToUZ)),
					rotatable: uploadZoneObj.rotatable,
					draggable: uploadZoneObj.draggable,
					resizable: uploadZoneObj.resizable,
					price: uploadZoneObj.price ? uploadZoneObj.price : parameters.price,
					replace: element._addToUZ,
					lockUniScaling: uploadZoneObj.lockUniScaling,
					uniScalingUnlockable: uploadZoneObj.uniScalingUnlockable,
					advancedEditing: uploadZoneObj.advancedEditing,
					originX: uploadZoneObj.originX,
					originY: uploadZoneObj.originY,
					angle: uploadZoneObj.angle
				}
			);

			//set some origin params that are needed when resetting element in UZ
			$.extend(parameters.originParams, {
				boundingBox: parameters.boundingBox,
				replace: parameters.replace,
				rotatable: parameters.rotatable,
				draggable: parameters.draggable,
				resizable: parameters.resizable,
				lockUniScaling: parameters.lockUniScaling,
				uniScalingUnlockable: parameters.uniScalingUnlockable,
				price: parameters.price,
				scaleX: parameters.scaleX,
				scaleY: parameters.scaleY,
				hasUploadZone: true,
				autoCenter: true,
				originX: parameters.originX,
				originY: parameters.originY,
				angle: parameters.angle
			});

			delete parameters[''];
			delete element['_addToUZ'];

		}

		//if topped, z-index can not be changed
		if(parameters.topped) {
			parameters.zChangeable = false;
		}

		//new element added
		if(FPDUtil.elementIsEditable(parameters)) {
			parameters.isEditable = parameters.evented = parameters.selectable = true;
		}

		//upload zones have no controls
		if(!parameters.uploadZone || instance.options.editorMode) {
			if(parameters.draggable) {
				parameters.lockMovementX = parameters.lockMovementY = false;
			}

			if(parameters.rotatable) {
				parameters.lockRotation = false;
				parameters.hasRotatingPoint = true;
			}

			if(parameters.resizable) {
				parameters.lockScalingX = parameters.lockScalingY = false;
			}

			if((parameters.resizable || parameters.rotatable || parameters.removable)) {
				parameters.hasControls = true;
			}

		}

		if(parameters.uploadZone) {

			if(!instance.options.editorMode) {

				if(parameters.uploadZoneMovable) {
					parameters.lockMovementX = parameters.lockMovementY = false;
				}

				if(parameters.uploadZoneRemovable) {
					parameters.removable = true;
					parameters.copyable = false;
					parameters.hasControls = true;
				}

			}

			if(fabric.version !== '3.0.0') {
				parameters.lockRotation = true;
				parameters.hasRotatingPoint = false;
			}

		}

		if(parameters.fixed) {

			if(FPDUtil.isEmpty(parameters.replace)) {
				parameters.replace = element.title;
			}

		}

		if(parameters.replace && parameters.replace != '') {

			var replacedElement = instance.getElementByReplace(parameters.replace);

			//element with replace in view found and replaced element is not the new element
			if(replacedElement !== null && replacedElement !== element ) {
				parameters.z = instance.getZIndex(replacedElement);
				parameters.left = element.originParams.left = replacedElement.left;
				parameters.top = element.originParams.top =  replacedElement.top;
				parameters.autoCenter = false;
				if(instance.options.applyFillWhenReplacing) {
					parameters.fill = replacedElement.fill;
				}
				instance.removeElement(replacedElement);
			}

		}

		//needs to before setOptions
		if(typeof parameters.text === 'string') {

			var text = parameters.text;

			//remove emojis
			if(instance.options.disableTextEmojis) {
				text = text.replace(FPDEmojisRegex, '');
				text = text.replace(String.fromCharCode(65039), ""); //fix: some emojis left a symbol with char code 65039
			}

			if(element.maxLength != 0 && text.length > element.maxLength) {
				text = text.substr(0, element.maxLength);
				element.selectionStart = element.maxLength;
			}

			//check lines length
			if(element.maxLines != 0) {

				if(element.type == 'textbox' && element.__lineHeights) {
					text = _maxTextboxLines(element, text);
				}
				else if(text.split("\n").length > element.maxLines) {
					text = text.replace(/([\s\S]*)\n/, "$1");
					element.exitEditing(); //exit editing when max lines are reached
				}

			}

			element.set('text', text);
			parameters.text = text;

			if(initialElementsLoaded && element.chargeAfterEditing) {

				if(!element._isPriced) {
					instance.changePrice(element.price, '+');
					element._isPriced = true;
				}

				if( element._initialText === text && element._isPriced) {
					instance.changePrice(element.price, '-');
					element._isPriced = false;
				}

			}

		}

		if(elemType === 'text') {

			if(parameters.hasOwnProperty('textDecoration')) {
				parameters.underline = parameters.textDecoration === 'underline';
			}

			if(parameters.letterSpacing !== undefined) {
				parameters.charSpacing = parameters.letterSpacing * 100;
			}

			if(parameters.fontSize && parameters.fontSize < element.minFontSize) {
				parameters.fontSize = element.minFontSize;
			}
			else if(parameters.fontSize && parameters.fontSize > element.maxFontSize) {
				parameters.fontSize = element.maxFontSize;
			}

			if(parameters.text) {

				if(element.textTransform === 'uppercase') {
					text = text.toUpperCase()
				}
				else if(element.textTransform === 'lowercase') {
					text = text.toLowerCase()
				}

				element.set('text', text);
				parameters.text = text;

			}

			if(parameters.textTransform) {

				var text = element.text;
				if(parameters.textTransform === 'uppercase') {
					text = text.toUpperCase()
				}
				else if(parameters.textTransform === 'lowercase') {
					text = text.toLowerCase()
				}

				element.set('text', text);
				parameters.text = text;

			}

			if((parameters.shadowColor || parameters.shadowBlur || parameters.shadowOffsetX || parameters.shadowOffsetY) && !element.shadow) {

				var shadowObj = {
					color: parameters.shadowColor ? parameters.shadowColor: 'rgba(0,0,0,0)'
				}

				element.setShadow(shadowObj);

			}

			if(element.shadow && parameters.hasOwnProperty('shadowColor')) {
				if(parameters.shadowColor) {
					element.shadow.color = parameters.shadowColor;
				}
				else {
					element.setShadow(null);
				}
			}

			if(element.shadow) {

				if(parameters.shadowBlur) {
					element.shadow.blur = parameters.shadowBlur;
				}

				if(parameters.shadowOffsetX) {
					element.shadow.offsetX = parameters.shadowOffsetX;
				}

				if(parameters.shadowOffsetY) {
					element.shadow.offsetY = parameters.shadowOffsetY;
				}

			}

		}

		delete parameters['paths']; //no paths in parameters
		element.setOptions(parameters);

		if(element.type == 'i-text' && element.widthFontSize && element.text.length > 0) {

			var resizedFontSize;
			if(element.width > element.widthFontSize) {
				resizedFontSize = element.fontSize * (element.widthFontSize / (element.width + 1)); //decrease font size
			}
			else {
				resizedFontSize = element.fontSize * (element.widthFontSize / (element.width - 1)); //increase font size
			}

			if(resizedFontSize < element.minFontSize) {
				resizedFontSize = element.minFontSize;
			}
			else if(resizedFontSize > element.maxFontSize) {
				resizedFontSize = element.maxFontSize;
			}

			resizedFontSize = parseInt(resizedFontSize);
			parameters.fontSize = resizedFontSize;
			element.set('fontSize', resizedFontSize);

		}

		//clip element
		if((element.boundingBox && parameters.boundingBoxMode === 'clipping') || parameters.hasUploadZone) {
			_clipElement(element);
		}

		if(parameters.autoCenter) {
			instance.centerElement(true, true, element);
		}

		//change element color
		if(parameters.fill !== undefined || parameters.svgFill !== undefined) {
			var fill = parameters.svgFill !== undefined ? parameters.svgFill : parameters.fill;
			instance.changeColor(element, fill);
			element.pattern = undefined;
		}

		//set pattern
		if(parameters.pattern !== undefined) {
			_setPattern(element, parameters.pattern);
			_setColorPrice(element, parameters.pattern);
		}

		//set filter
		if(parameters.filter) {

			element.filters = [];
			var fabricFilter = FPDUtil.getFilter(parameters.filter);

			if(fabricFilter != null) {
				element.filters.push(fabricFilter);
			}
			if(typeof element.applyFilters !== 'undefined') {
				element.applyFilters();
			}

		}

		//set z position, check if element has canvas prop, otherwise its not added into canvas
		if(element.canvas && parameters.z >= 0) {
			element.moveTo(parameters.z);
			_bringToppedElementsToFront();
		}

		if(element.curved) {

			if(parameters.curveRadius) {
				element.set('radius', parameters.curveRadius);
			}

			if(parameters.curveSpacing) {
				element.set('spacing', parameters.curveSpacing);
			}

			if(parameters.curveReverse !== undefined) {
				element.set('reverse', parameters.curveReverse);
			}

		}

		if(element.uploadZone) {
			element.evented = element.opacity !== 0;
		}
		else if(element.isEditable && !instance.options.editorMode) {
			element.evented = !parameters.locked;
		}

		if(instance.options.cornerControlsStyle == 'basic' && element.lockScalingX && element.lockScalingY) {

			element.setControlsVisibility({
		         mt: false,
		         mb: false,
		         ml: false,
		         mr: false,
		         bl: false,
		         br: false,
		         tl: false,
		         tr: false,
		    });

		}


		//check if a upload zone contains an object
		var objects = instance.stage.getObjects();
		for(var i=0; i < objects.length; ++i) {

			var object = objects[i];

			if(object.uploadZone && object.title == parameters.replace) {
				object.opacity = 0;
				object.evented = false;
			}

		}

		element.setCoords();
		instance.stage.renderAll().calcOffset();

		$this.trigger('elementModify', [element, parameters]);

		_checkContainment(element);

		//select element
		if(parameters.autoSelect && element.isEditable && !instance.options.editorMode && $(instance.stage.getElement()).is(':visible')) {

			setTimeout(function() {
				instance.stage.setActiveObject(element);
				instance.stage.renderAll();
			}, 350);

		}

	};

	/**
	 * Returns the bounding box of an element.
	 *
	 * @method getBoundingBoxCoords
	 * @param {fabric.Object} element A fabric object
	 * @return {Object | Boolean} The bounding box object with x,y,width and height or false.
	 */
	this.getBoundingBoxCoords = function(element) {

		if(element.boundingBox || element.uploadZone) {

			if(typeof element.boundingBox == "object") {


				if( element.boundingBox.hasOwnProperty('x') &&
					element.boundingBox.hasOwnProperty('y') &&
					element.boundingBox.width &&
					element.boundingBox.height
				) {
					return {
						left: element.boundingBox.x,
						top: element.boundingBox.y,
						width: element.boundingBox.width,
						height: element.boundingBox.height
					};
				}
				else {
					return false;
				}

			}
			else {

				var objects = instance.stage.getObjects();

				for(var i=0; i < objects.length; ++i) {

					//get all layers from first view
					var object = objects[i];
					if(element.boundingBox == object.title) {

						var topLeftPoint = object.getPointByOrigin('left', 'top');

						return {
							left: topLeftPoint.x,
							top: topLeftPoint.y,
							width: object.width * object.scaleX,
							height: object.height * object.scaleY,
							angle: object.angle || 0
						};

						break;
					}

				}

			}

		}

		return false;

	};

	/**
	 * Creates a data URL of the view.
	 *
	 * @method toDataURL
	 * @param {Function} callback A function that will be called when the data URL is created. The function receives the data URL.
	 * @param {String} [backgroundColor=transparent] The background color as hexadecimal value. For 'png' you can also use 'transparent'.
	 * @param {Object} [options] See fabricjs documentation http://fabricjs.com/docs/fabric.Canvas.html#toDataURL.
	 * @param {Boolean} [options.onlyExportable=false] If true elements with excludeFromExport=true are not exported in the image.
	 * @param {fabric.Image} [watermarkImg=null] A fabricJS image that includes the watermark image.
	 * @param {Boolean} [deselectElement=true] Deselect current selected element.
	 */
	this.toDataURL = function(callback, backgroundColor, options, watermarkImg, deselectElement) {

		callback = callback === undefined ? function() {} : callback;
		backgroundColor = backgroundColor === undefined ? 'transparent' : backgroundColor;
		options = options === undefined ? {} : options;
		options.onlyExportable = options.onlyExportable === undefined ? false : options.onlyExportable;
		options.multiplier = options.multiplier === undefined ? 1 : options.multiplier;
		options.enableRetinaScaling = options.enableRetinaScaling === undefined ? false : options.enableRetinaScaling;
		watermarkImg = watermarkImg === undefined ? false : watermarkImg;
		deselectElement = deselectElement === undefined ? true : deselectElement;

		var invisibleObjs = ['_snap_lines_group', '_ruler_hor', '_ruler_ver'],
			hiddenObjs = [],
			 tempHighlightEditableObjects = instance.options.highlightEditableObjects;

		instance.options.highlightEditableObjects = 'transparent';
		instance.stage.getObjects().forEach(function(obj) {

			if(invisibleObjs.indexOf(obj.id) !== -1 || (obj.excludeFromExport && options.onlyExportable)) {

				obj.visible = false;
				hiddenObjs.push(obj);

			}

		});

		if(deselectElement) {
			instance.deselectElement();
		}

		var tempDevicePixelRatio = fabric.devicePixelRatio;
		fabric.devicePixelRatio = 1;

		instance.stage.setDimensions({width: instance.options.stageWidth, height: instance.options.stageHeight}).setZoom(1);

		//scale view mask to multiplier
		if(instance.maskObject && instance.maskObject._originParams) {
			instance.maskObject.left = instance.maskObject._originParams.left * options.multiplier;
			instance.maskObject.top = instance.maskObject._originParams.top * options.multiplier;
			instance.maskObject.scaleX = instance.maskObject._originParams.scaleX * options.multiplier;
			instance.maskObject.scaleY = instance.maskObject._originParams.scaleY * options.multiplier;
			instance.maskObject.setCoords();
		}

		instance.stage.setBackgroundColor(backgroundColor, function() {

			if(watermarkImg) {
				instance.stage.add(watermarkImg);
				watermarkImg.center();
				watermarkImg.bringToFront();
			}

			//get data url
			callback(instance.stage.toDataURL(options));

			if(watermarkImg) {
				instance.stage.remove(watermarkImg);
			}

			if($(instance.stage.wrapperEl).is(':visible')) {
				instance.resetCanvasSize();
			}

			instance.stage.setBackgroundColor('transparent', function() {
				instance.stage.renderAll();
			});

			for(var i=0; i<hiddenObjs.length; ++i) {
				hiddenObjs[i].visible = true;
			}

			instance.stage.renderAll();

			fabric.devicePixelRatio = tempDevicePixelRatio;
			instance.options.highlightEditableObjects = tempHighlightEditableObjects;

		});

	};

	/**
	 * Returns the view as SVG.
	 *
	 * @method toSVG
	 * @param {Object} options See fabricjs documentation http://fabricjs.com/docs/fabric.Canvas.html#toSVG
	 * @param {Function} reviver See fabricjs documentation http://fabricjs.com/docs/fabric.Canvas.html#toSVG
	 * @param {Boolean} respectPrintingBox Only generate SVG from printing box
	 * @param {fabric.Image} [watermarkImg=null] A fabricJS image that includes the watermark image.
	 * @param {Array} [fontsToEmbed=[]] Aan array containing fonts to embed in the SVG. You can use <a href="https://jquerydoc.fancyproductdesigner.com/classes/FancyProductDesigner.html#method_getUsedColors" target="_blank">getUsedFonts method</a>
	 * @return {String} A XML representing a SVG.
	 */
	this.toSVG = function(options, reviver, respectPrintingBox, watermarkImg, fontsToEmbed) {

		options = options === undefined ? {} : options;
		respectPrintingBox = respectPrintingBox === undefined ? false : respectPrintingBox;
		watermarkImg = watermarkImg === undefined ? null : watermarkImg;
		fontsToEmbed = fontsToEmbed === undefined ? [] : fontsToEmbed;

		var svg;

		instance.deselectElement();
		if(respectPrintingBox && FPDUtil.objectHasKeys(instance.options.printingBox, ['left','top','width','height'])) {

			var offsetX = 0,
				offsetY = 0;

			if(FPDUtil.objectHasKeys(instance.options.output, ['bleed', 'width', 'height'])) {
				offsetX = (instance.options.output.bleed / instance.options.output.width) * instance.options.printingBox.width,
				offsetY = (instance.options.output.bleed / instance.options.output.height) * instance.options.printingBox.height;
			}

			options.viewBox = {
				x: instance.options.printingBox.left - offsetX,
				y: instance.options.printingBox.top - offsetY,
				width: instance.options.printingBox.width + (offsetX * 2),
				height: instance.options.printingBox.height  + (offsetY * 2)
			};

			instance.stage.setDimensions({width: instance.options.printingBox.width, height: instance.options.printingBox.height}).setZoom(1);
		}
		else {
			instance.stage.setDimensions({width: instance.options.stageWidth, height: instance.options.stageHeight}).setZoom(1);
		}

		//remove background, otherwise unneeeded rect is added in the svg
		var tempCanvasBackground = instance.stage['backgroundColor'];
		if(tempCanvasBackground == 'transparent') {
			instance.stage['backgroundColor'] = false;
		}

		if(watermarkImg) {
			instance.stage.add(watermarkImg);
			watermarkImg.center();
			watermarkImg.bringToFront();
		}

		svg = instance.stage.toSVG(options, reviver);

		if(watermarkImg) {
			instance.stage.remove(watermarkImg);
		}

		instance.stage['backgroundColor'] = tempCanvasBackground;

		if($(instance.stage.wrapperEl).is(':visible')) {
			instance.resetCanvasSize();
		}

		var $svg = $(svg);

		//move clipPath to defs section
		$svg.find('clipPath').appendTo($svg.children('defs'));

		//store fonts in style tag
		$svg.children('defs').append('<style type="text/css"></style>');
		var googleFontsUrl = '',
			customFontsStr = '';

		fontsToEmbed.forEach(function(fontItem) {

			if(fontItem.hasOwnProperty('url')) {

				if(fontItem.url == 'google') {
					googleFontsUrl += fontItem.name.replace(/\s/g, "+") + ':ital,wght@0,400;0,700;1,700&';
				}
				else {
					customFontsStr += FPDUtil.parseFontsToEmbed(fontItem);
				}

			}
		})

		if(googleFontsUrl.length > 0) {
			$svg.find('defs > style').append('@import url("https://fonts.googleapis.com/css2?family='+googleFontsUrl.replace(/&/g, "&amp;")+'display=swap");');
		}
		if(customFontsStr.length > 0) {
			$svg.find('defs > style').append(customFontsStr);
		}

		svg = $('<div>').append(
			$svg.clone()).html()
			//replace all newlines
			.replace(/(?:\r\n|\r|\n)/g, '')
			//replace & with escaped string for google fonts url, otherwise syntax error
			.replace(/700&/g, "700&amp;"
		);

		return svg;

	};

	/**
	 * Removes the canvas and resets all relevant view properties.
	 *
	 * @method duplicate
	 * @param {fabric.Object} [element] The element to duplicate. If not set, it duplicates the current selected element.
	 */
	this.duplicate = function(element) {

		element = element === undefined ? instance.stage.getActiveObject() : element;

		var newOpts = instance.getElementJSON(element);

		newOpts.top = newOpts.top + 30;
		newOpts.left = newOpts.left + 30;

		if(!instance.options.editorMode) {
			newOpts.autoSelect = true;
		}

		instance.addElement(
			FPDUtil.getType(element.type),
			element.source,
			'Copy '+element.title,
			newOpts
		);

	};

	/**
	 * Removes the canvas and resets all relevant view properties.
	 *
	 * @method reset
	 */
	this.reset = function(removeCanvas) {

		removeCanvas = removeCanvas === undefined ? true : removeCanvas;

		instance.undos = [];
		instance.redos = [];
		instance.elements = [];
		instance.totalPrice = instance.truePrice = instance.additionalPrice = 0;
		instance.stage.clear();

		if(removeCanvas) {
			instance.stage.wrapperEl.remove();
		}

		$this.trigger('clear');
		$this.trigger('priceChange', [0, 0]);

	};

	/**
	 * Undo the last change.
	 *
	 * @method undo
	 */
	this.undo = function() {

		if(instance.undos.length > 0) {

			var last = instance.undos.pop();

			//check if element was removed
			if(last.interaction === 'remove') {
				instance.stage.add(last.element);
				last.interaction = 'add';
				$this.trigger('elementAdd', [last.element]);
			}
			else if(last.interaction === 'add') {

				var hasReplace = last.element.replace;
				instance.stage.remove(last.element);
				last.interaction = 'remove';
				$this.trigger('elementRemove', [last.element]);

				if(hasReplace && instance.undos.length && instance.undos[instance.undos.length-1].element.replace == hasReplace) {
					last = instance.undos.pop();
					instance.stage.add(last.element);
					$this.trigger('elementAdd', [last.element]);
				}

			}

			if(!last.element._ignore) {
				_setUndoRedo(false, {
					element: last.element,
					parameters: instance.getElementJSON(last.element),
					interaction: last.interaction
				});
			}

			instance.setElementParameters(last.parameters, last.element, false);

			this.deselectElement();
			_elementHasUploadZone(last.element);

		}

		return instance.undos;

	};

	/**
	 * Redo the last change.
	 *
	 * @method redo
	 */
	this.redo = function() {

		if(instance.redos.length > 0) {

			var last = instance.redos.pop();

			if(last.interaction === 'remove') {
				instance.stage.add(last.element);
				last.interaction = 'add';
				$this.trigger('elementAdd', [last.element]);
			}
			else if(last.interaction === 'add') {
				instance.stage.remove(last.element);
				last.interaction = 'remove';
				$this.trigger('elementRemove', [last.element]);
			}

			if(!last.element._ignore) {
				_setUndoRedo({
					element: last.element,
					parameters: instance.getElementJSON(last.element),
					interaction: last.interaction
				});
			}

			instance.setElementParameters(last.parameters, last.element, false);

			this.deselectElement();
			_elementHasUploadZone(last.element);

		}

		return instance.redos;

	};

	/**
	 * Get the canvas(stage) JSON.
	 *
	 * @method getJSON
	 * @return {Object} An object with properties.
	 */
	this.getJSON = function() {

		var parameterKeys = fpdOptions.getParameterKeys();

		parameterKeys = parameterKeys.concat(FancyProductDesignerView.propertiesToInclude);

		return instance.stage.toJSON(parameterKeys);

	};

	/**
	 * Resizes the canvas responsive.
	 *
	 * @method resetCanvasSize
	 */
	this.resetCanvasSize = function() {

		instance.responsiveScale = $productStage.outerWidth() < instance.options.stageWidth ? $productStage.outerWidth() / instance.options.stageWidth : 1;

		if(!isNaN(instance.options.maxCanvasHeight) && instance.options.maxCanvasHeight !== 1) {

			var maxHeight = window.innerHeight * parseFloat(instance.options.maxCanvasHeight);
			if(instance.options.stageHeight > instance.options.stageWidth && (instance.options.stageHeight * instance.responsiveScale) > maxHeight) {
				instance.responsiveScale = maxHeight / instance.options.stageHeight;
			}

		}

		instance.responsiveScale = parseFloat(Number(instance.responsiveScale.toFixed(7)));
		instance.responsiveScale = Math.min(instance.responsiveScale, 1);

		if(!instance.options.responsive) {
			instance.responsiveScale = 1;
		}

		if(!instance.options.editorMode && instance.maskObject && instance.maskObject._originParams) {
			instance.maskObject.left = instance.maskObject._originParams.left * instance.responsiveScale;
			instance.maskObject.top = instance.maskObject._originParams.top * instance.responsiveScale;
			instance.maskObject.scaleX = instance.maskObject._originParams.scaleX * instance.responsiveScale;
			instance.maskObject.scaleY = instance.maskObject._originParams.scaleY * instance.responsiveScale;

		}
		else if(instance.maskObject) {
			instance.maskObject.setCoords();
		}


		instance.stage
		.setDimensions({
			width: $productStage.width(),
			height: instance.options.stageHeight * instance.responsiveScale
		})
		.setZoom(instance.responsiveScale)
		.calcOffset()
		.renderAll();

		$productStage.height(instance.stage.height)
		.parent('.fpd-main-wrapper').css('min-height', instance.stage.height);

		var $container = $productStage.parents('.fpd-container:first');
		if($container.length > 0) {
			$container.height($container.hasClass('fpd-sidebar') ? instance.stage.height : 'auto');
			$container.width($container.hasClass('fpd-topbar') ? instance.options.stageWidth : 'auto');
		}

		return instance.responsiveScale;

	};

	/**
	 * Gets an elment by replace property.
	 *
	 * @method getElementByReplace
	 */
	this.getElementByReplace = function(replaceValue) {

		var objects = instance.stage.getObjects();
		for(var i = 0; i < objects.length; ++i) {
			var object = objects[i];
			if(object.replace === replaceValue) {
				return object;
				break;
			}
		}

		return null;

	};

	/**
	 * Gets the JSON of an element.
	 *
	 * @method getElementJSON
	 * @param {String} [element] The target element. If not set, it it will use the current selected.
	 * @param {Boolean} [addPropertiesToInclude=false] Include the properties from {{#crossLink "FancyProductDesignerView/propertiesToInclude:property"}}{{/crossLink}}.
	 * @return {Object} An object with properties.
	 */
	this.getElementJSON = function(element, addPropertiesToInclude) {

		element = element === undefined ? instance.stage.getActiveObject() : element;
		addPropertiesToInclude = addPropertiesToInclude === undefined ? false : addPropertiesToInclude;

		if(!element) { return {}; }

		var properties = Object.keys(instance.options.elementParameters),
			additionalKeys  = FPDUtil.getType(element.type) === 'text' ? Object.keys(instance.options.textParameters) : Object.keys(instance.options.imageParameters);

		properties = $.merge(properties, additionalKeys);

		if(addPropertiesToInclude) {
			properties = $.merge(properties, FancyProductDesignerView.propertiesToInclude);
		}

		if(element.uploadZone) {
			properties.push('customAdds');
			properties.push('designCategories');
			properties.push('designCategories[]'); //fpd-admin
		}

		if(FPDUtil.getType(element.type) === 'text') {
			properties.push('text');
			properties.push('_initialText');
		}

		if(element.type === FPDPathGroupName) {
			properties.push('svgFill');
		}

		properties.push('width');
		properties.push('height');
		properties.push('isEditable');
		properties.push('hasUploadZone');
		properties.push('clippingRect');
		properties.push('evented');
		properties.push('isCustom');
		properties.push('currentColorPrice');
		properties.push('_isPriced');
		properties.push('originParams');
		properties.push('originSource');
		properties.push('depositphotos');
		properties = properties.sort();

		var topLeftPoint = element.getPointByOrigin('left', 'top');
		if(addPropertiesToInclude) {

			var json = element.toJSON(properties);
			json.topLeftX = topLeftPoint.x;
			json.topLeftY = topLeftPoint.y;

			return json;

		}
		else {

			var json = {};
			for(var i=0; i < properties.length; ++i) {
				var prop = properties[i];
				if(element[prop] !== undefined) {
					json[prop] = element[prop];
				}

			}

			json.topLeftX = topLeftPoint.x;
			json.topLeftY = topLeftPoint.y;

			return json;
		}

	};

	/**
	 * Centers an element horizontal or/and vertical.
	 *
	 * @method centerElement
	 * @param {Boolean} h Center horizontal.
	 * @param {Boolean} v Center vertical.
	 * @param {fabric.Object} [element] The element to center. If not set, it centers the current selected element.
	 */
	this.centerElement = function(h, v, element) {

		element = typeof element === 'undefined' ? instance.stage.getActiveObject() : element;

		_centerObject(element, h, v);
		element.autoCenter = false;

	};

	/**
	 * Aligns an element.
	 *
	 * @method alignElement
	 * @param {String} pos Allowed values: left, right, top or bottom.
	 * @param {fabric.Object} [element] The element to center. If not set, it centers the current selected element.
	 */
	this.alignElement = function(pos, element) {

		element = typeof element === 'undefined' ? instance.stage.getActiveObject() : element;

		var localPoint = element.getPointByOrigin('left', 'top'),
			boundingBox = instance.getBoundingBoxCoords(element),
			posOriginX = 'left',
			posOriginY = 'top';

		if(pos === 'left') {

			localPoint.x = boundingBox ? boundingBox.left : 0;
			localPoint.x += element.padding + 1;

		}
		else if(pos === 'top') {

			localPoint.y = boundingBox ? boundingBox.top : 0;
			localPoint.y += element.padding + 1;

		}
		else if(pos === 'right') {

			localPoint.x = boundingBox ? boundingBox.left + boundingBox.width - element.padding : instance.options.stageWidth - element.padding;
			localPoint.x -= FPDUtil.getType(element.type) == 'text' ? 4 : 0;
			posOriginX = 'right';

		}
		else {

			localPoint.y = boundingBox ? boundingBox.top + boundingBox.height - element.padding : instance.options.stageHeight;
			localPoint.y -= FPDUtil.getType(element.type) == 'text' ? 4 : 0;
			posOriginY = 'bottom';

		}

		element.setPositionByOrigin(localPoint, posOriginX, posOriginY);

		instance.stage.renderAll();
		element.setCoords();

		_checkContainment(element);

	};

	/**
	 * Gets the z-index of an element.
	 *
	 * @method getZIndex
	 * @param {fabric.Object} [element] The element to center. If not set, it centers the current selected element.
	 * @return {Number} The index.
	 */
	this.getZIndex = function(element) {

		element = typeof element === 'undefined' ? instance.stage.getActiveObject() : element;

		var objects = instance.stage.getObjects();
		return objects.indexOf(element);
	};

	/**
	 * Changes the color of an element.
	 *
	 * @method changeColor
	 * @param {fabric.Object} element The element to colorize.
	 * @param {String} hex The color.
	 * @param {Boolean} colorLinking Use color linking.
	 */
	this.changeColor = function(element, hex, colorLinking) {

		colorLinking = typeof colorLinking === 'undefined' ? true : colorLinking;

		var colorizable = FPDUtil.elementIsColorizable(element);

		//check if hex color has only 4 digits, if yes, append 3 more
		if(typeof hex === 'string' && hex.length == 4) {
			hex += hex.substr(1, hex.length);
		}

		//text
		if(FPDUtil.getType(element.type) === 'text') {

			hex = hex === false ? '#000000' : hex;
			if(typeof hex == 'object') {
				hex = hex[0];
			}

			//set color of a text element
			element.set('fill', hex);
			instance.stage.renderAll();

			element.pattern = null;
			element.fill = hex;

		}
		//path groups (svg)
		else if(element.type == FPDPathGroupName && typeof hex == 'object') {

			for(var i=0; i < hex.length; ++i) {
				if(element.getObjects()[i]) {
					element.getObjects()[i].set('fill', hex[i]);
				}

			}

			instance.stage.renderAll();

			element.svgFill = hex;
			delete element['fill'];

		}
		//image
		else {

			if(typeof hex == 'object') {
				hex = hex[0];
			}

			if(typeof hex !== 'string') {
				hex = false;
			}

			//colorize png or dataurl image
			if(colorizable == 'png' || colorizable == 'dataurl') {

				element.filters = [];

				//fix: fabricjs 2.+ when element is custom element and changing base products
				setTimeout(function() {

					if(hex) {
						element.filters.push(new fabric.Image.filters.BlendColor({mode: 'tint', color: hex}));
					}

					element.applyFilters();
					instance.stage.renderAll();

					$this.trigger('elementColorChange', [element, hex, colorLinking]);

				}, 1);

				element.fill = hex;

			}
			//colorize svg (single path)
			else if(colorizable == 'svg') {

				element.set('fill', hex);
				instance.stage.renderAll();

				$this.trigger('elementColorChange', [element, hex, colorLinking]);

			}


		}

		_setColorPrice(element, hex);

		/**
	     * Gets fired when the color of an element is changing.
	     *
	     * @event FancyProductDesignerView#elementColorChange
	     * @param {Event} event
	     * @param {fabric.Object} element
	     * @param {String} hex
	     * @param {Boolean} colorLinking
	     */
		$this.trigger('elementColorChange', [element, hex, colorLinking]);

	};

	/**
	 * Gets the index of the view.
	 *
	 * @method getIndex
	 * @return {Number} The index.
	 */
	this.getIndex = function() {

		return $productStage.children('.fpd-view-stage').index(instance.stage.wrapperEl);

	};

	/**
	 * Gets an upload zone by title.
	 *
	 * @method getUploadZone
	 * @param {String} title The target title of an element.
	 * @return {fabric.Object} A fabric object representing the upload zone.
	 */
	this.getUploadZone = function(title) {

		var objects = instance.stage.getObjects();

		for(var i=0; i < objects.length; ++i) {

			if(objects[i].uploadZone && objects[i].title == title) {
				return objects[i];
				break;
			}

		}

	};

	/**
	 * Changes the price by an operator, + or -.
	 *
	 * @method changePrice
	 * @param {Number} price Price as number.
	 * @param {String} operator "+" or "-".
	 * @return {Number} The total price of the view.
	 */
	this.changePrice = function(price, operator, additionalPrice) {

		additionalPrice = additionalPrice === undefined ? null : additionalPrice;

		if(typeof price !== 'number') {
			price = Number(price);
		}

		if(operator === '+') {
			instance.totalPrice += price;
		}
		else {
			instance.totalPrice -= price;
		}

		if(additionalPrice !== null) {

			var tempAdditionalPrice = instance.additionalPrice;
			instance.totalPrice -= tempAdditionalPrice;

			instance.additionalPrice = additionalPrice;
			instance.totalPrice += additionalPrice;

		}

		instance.truePrice = instance.totalPrice;

		//consider max. view price
		if(typeof instance.options.maxPrice === 'number' && instance.options.maxPrice != -1 && instance.truePrice > instance.options.maxPrice) {
			instance.truePrice = Number(instance.options.maxPrice);
		}

		//price has decimals, set max. decimals to 2
		if(instance.truePrice % 1 != 0) {
			instance.truePrice = Number(instance.truePrice.toFixed(2));
		}

		/**
	     * Gets fired as soon as the price has changed.
	     *
	     * @event FancyProductDesignerView#priceChange
	     * @param {Event} event
	     * @param {number} elementPrice - The price of the added element.
	     * @param {number} truePrice - The total price.
	     */
		$this.trigger('priceChange', [price, instance.truePrice]);

		return instance.truePrice;

	};

	/**
	 * Use a SVG image as mask for the whole view. The image needs to be a SVG file with only one path. The method toSVG() does not include the mask.
	 *
	 * @method setMask
	 * @param {Object|Null} maskOptions An object containing the URL to the svg. Optional: scaleX, scaleY, left and top.
	 */
	this.setMask = function(maskOptions, callback) {

		callback = typeof callback !== 'undefined' ? callback : function() {};

		if(maskOptions && maskOptions.url && $.inArray('svg', maskOptions.url.split('.')) != -1) {

			instance.mask = maskOptions;

			var timeStamp = Date.now().toString(),
				_loadFromScript = instance.options._loadFromScript ? instance.options._loadFromScript : '',
				url = _loadFromScript + maskOptions.url;

			if(instance.options.imageLoadTimestamp && !instance.options._loadFromScript) {
				url += '?'+timeStamp;
			}

			//check if url is available
			$.get(url)
			.done(function(data) {

				fabric.loadSVGFromURL(url, function(objects, options) {

					var svgGroup = null;
					if(objects) {
						//if objects is null, svg is loaded from external server with cors disabled
						svgGroup = objects ? fabric.util.groupSVGElements(objects, options) : null;

						svgGroup.setOptions({
							left: maskOptions.left ? Number(maskOptions.left) :  0,
							top: maskOptions.top ? Number(maskOptions.top) :  0,
							scaleX: maskOptions.scaleX ? Number(maskOptions.scaleX) :  1,
							scaleY: maskOptions.scaleY ? Number(maskOptions.scaleY) :  1,
							selectable: true,
							evented: false,
							resizable: true,
							lockUniScaling: false,
							lockRotation: true,
							borderColor: 'transparent',
							fill: 'rgba(0,0,0,0)',
							transparentCorners: true,
							cornerColor: instance.options.selectedColor,
							cornerIconColor: instance.options.cornerIconColor,
							cornerSize: 24,
							originX: 'left',
							originY: 'top',
							name: "view-mask",
							objectCaching: false,
							excludeFromExport: true,
							_ignore: true,
							_originParams: {
								left: maskOptions.left ? Number(maskOptions.left) :  0,
								top: maskOptions.top ? Number(maskOptions.top) :  0,
								scaleX: maskOptions.scaleX ? Number(maskOptions.scaleX) :  1,
								scaleY: maskOptions.scaleY ? Number(maskOptions.scaleY) :  1,
							}
						})

						instance.stage.clipTo = function(ctx) {
						  svgGroup.render(ctx);
						};
						instance.stage.renderAll();

						instance.maskObject = svgGroup;
						instance.resetCanvasSize();
					}

					callback(svgGroup);

				});

			})
			.fail(callback);

		}
		else {
			instance.stage.clipTo = instance.maskObject = instance.mask = null;
			instance.stage.renderAll();
		}

	};

	/**
	 * Returns all options with the keys that are set in FancyProductDesignerView.relevantOptions property.
	 *
	 * @method getOptions
	 * @return {Object} An object containing all relevant options.
	 */
	this.getOptions = function() {

		var options = {};

		if(typeof FancyProductDesignerView.relevantOptions === 'object') {

			FancyProductDesignerView.relevantOptions.forEach(function(key) {
				options[key] = instance.options[key];
			});

		}

		return options;

	};

	/**
	 * Toggles the lockment of view. If the view is locked, the price of the view will not be added to the total product price.
	 *
	 * @method toggleLock
	 * @param {Boolean} toggle The toggle state.
	 * @return {Boolean} The toggle state.
	 */
	this.toggleLock = function(toggle) {

		toggle = toggle === undefined ? true : toggle;

		instance.locked = toggle;

		$this.trigger('priceChange', [0, instance.truePrice]);

		return toggle;

	};

	/**
	 * Removes the current elements and loads a set of new elements into the view.
	 *
	 * @param {Array} elements An array containing elements.
	 * @param {Function} callback A function that will be called when all elements have beed added.
	 * @method loadElements
	 */
	this.loadElements = function(elements, callback) {

		if(initialElementsLoaded) {
			instance.reset(false);
		}

		instance.elements = [];

		instance.addElements(elements, callback);

	};

	/**
	 * Adds a set of elements into the view.
	 *
	 * @param {Array} elements An array containing elements.
	 * @param {Function} callback A function that will be called when all elements have beed added.
	 * @method addElements
	 */
	this.addElements = function(elements, callback) {

		var countElements = 0;

		//iterative function when element is added, add next one
		function _onElementAdded() {

			countElements++;

			//add all elements of a view
			if(countElements < elements.length) {
				var element = elements[countElements];
				if(!_removeNotValidElementObj(element)) {
					instance.addElement( element.type, element.source, element.title, element.parameters);
				}

			}
			//all initial elements are added, view is created
			else {

				instance.undos = [];
				instance.redos = [];
				$this.trigger('undoRedoSet', [instance.undos, instance.redos]);

				$this.off('elementAdd', _onElementAdded);
				if(typeof callback !== 'undefined') {
					callback.call(callback, instance);
				}

			}

		};

		function _removeNotValidElementObj(element) {

			if(element.type === undefined || element.source === undefined || element.title === undefined) {

				var removeInd = elements.indexOf(element)
				if(removeInd !== -1) {
					FPDUtil.log('Element index '+removeInd+' from elements removed, its not a valid element object!', 'info');
					_onElementAdded();
					return true;
				}

			}
			else {
				instance.elements.push(element);
			}

			return false;

		};

		var element = elements[0];
		//check if view contains at least one element
		if(element) {

			//listen when element is added
			$this.on('elementAdd', _onElementAdded);
			//add first element of view
			if(!_removeNotValidElementObj(element)) {
				instance.addElement( element.type, element.source, element.title, element.parameters);
			}

		}
		//no elements in view, view is created without elements
		else {
			if(typeof callback !== 'undefined') {
				callback.call(callback, instance);
			}
		}

	};


	this.renderElementBoundingBox = function(element) {

		if(instance.currentBoundingObject) {
			instance.stage.remove(instance.currentBoundingObject);
			instance.currentBoundingObject = null;
		}

		if(element) {

			var bbCoords = instance.getBoundingBoxCoords(element);
			if(bbCoords) {

				var boundingBoxProps = {
					left: bbCoords.left,
					top: bbCoords.top,
					width: bbCoords.width,
					height: bbCoords.height,
					angle: bbCoords.angle || 0,
					stroke: instance.options.boundingBoxColor,
					strokeWidth: 1,
					strokeLineCap: 'square',
					strokeDashArray: [10, 10],
					fill: false,
					selectable: false,
					evented: false,
					originX: 'left',
					originY: 'top',
					name: "bounding-box",
					excludeFromExport: true,
					_ignore: true
				};

				boundingBoxProps = $.extend({}, boundingBoxProps, instance.options.boundingBoxProps);
				instance.currentBoundingObject = new fabric.Rect(boundingBoxProps);

				instance.stage.add(instance.currentBoundingObject);
				instance.currentBoundingObject.bringToFront();

				/**
			     * Gets fired when bounding box is toggling.
			     *
			     * @event FancyProductDesignerView#boundingBoxToggle
			     * @param {Event} event
			     * @param {fabric.Object} currentBoundingObject - The current bounding box object.
			     * @param {Boolean} state
			     */
				$this.trigger('boundingBoxToggle', [instance.currentBoundingObject, true]);

			}
			else {
				element.clipTo = null;
			}

			_checkContainment(element);

		}

	};

	this.renderPrintingBox = function() {

		if(instance.printingBoxObject) {
			instance.stage.remove(instance.printingBoxObject);
			instance.printingBoxObject = null;
		}

		if(FPDUtil.objectHasKeys(instance.options.printingBox, ['left','top','width','height'])) {

			var printingBox = new fabric.Rect({
				left: 0,
				top: 0,
				width: instance.options.printingBox.width,
				height: instance.options.printingBox.height,
				stroke: instance.options.printingBox.visibility || instance.options.editorMode ? '#db2828' : 'transparent',
				strokeWidth: 1,
				strokeLineCap: 'square',
				fill: false,
				originX: 'left',
				originY: 'top',
				name: "printing-box",
				excludeFromExport: true,
				_ignore: true
			});

			instance.printingBoxObject = new fabric.Group([printingBox], {
				left: instance.options.printingBox.left,
				top: instance.options.printingBox.top,
				evented: false,
				resizable: true,
				lockUniScaling: true,
				lockRotation: true,
				borderColor: 'transparent',
				transparentCorners: true,
				cornerColor: instance.options.selectedColor,
				cornerIconColor: instance.options.cornerIconColor,
				cornerSize: 24,
				originX: 'left',
				originY: 'top',
				name: "printing-boxes",
				excludeFromExport: true,
				selectable: false,
				_ignore: true
			});

			instance.stage.add(instance.printingBoxObject);
			instance.printingBoxObject.setCoords();
			instance.stage.renderAll();

		}

	}

	/**
	 * This method needs to be called after the instance of {{#crossLink "FancyProductDesignerView"}}{{/crossLink}} is set.
	 *
	 * @method setup
	 */
	this.setup = function() {

		this.loadElements(view.elements, _afterSetup);

	};

	_initialize();

};

/**
 * Relevant options for the view when saving order data.
 *
 * @property relevantOptions
 * @type Array
 * @static
 * @default ['stageWidth',
	'stageHeight',
	'customAdds',
	'customImageParameters',
	'customTextParameters',
	'maxPrice',
	'optionalView',
	'designCategories',
	'printingBox',
	'output',
	'layouts',
	'usePrintingBoxAsBounding']
 */
FancyProductDesignerView.relevantOptions = [
	'stageWidth',
	'stageHeight',
	'customAdds',
	'customImageParameters',
	'customTextParameters',
	'maxPrice',
	'optionalView',
	'designCategories',
	'printingBox',
	'output',
	'layouts',
	'usePrintingBoxAsBounding',
	'threeJsPreviewModel'
];

/**
 * Properties to include when using the {{#crossLink "FancyProductDesignerView/getJSON:method"}}{{/crossLink}} or {{#crossLink "FancyProductDesignerView/getElementJSON:method"}}{{/crossLink}}.
 *
 * @property propertiesToInclude
 * @type Array
 * @static
 * @default ['_isInitial', 'lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY', 'lockScalingFlip', 'lockUniScaling', 'resizeType', 'clipTo', 'clippingRect', 'boundingBox', 'boundingBoxMode', 'selectable', 'evented', 'title', 'editable', 'cornerColor', 'cornerIconColor', 'borderColor', 'isEditable', 'hasUploadZone']
 */
FancyProductDesignerView.propertiesToInclude = ['_isInitial', 'lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY', 'lockScalingFlip', 'lockUniScaling', 'resizeType', 'clipTo', 'clippingRect', 'boundingBox', 'boundingBoxMode', 'selectable', 'evented', 'title', 'editable', 'cornerColor', 'cornerIconColor', 'borderColor', 'isEditable', 'hasUploadZone'];

var FPDFilters =  {

	none: {
		name: 'None',
		preview: 'data:image/png;base64, '
	},

	grayscale: {
		name: 'Grayscale',
		preview: 'data:image/png;base64, '
	},

	sepia: {
		name: 'Sepia',
		preview: 'data:image/png;base64, '
	},

	sepia2: {
		name: 'Sepia Second',
		preview: 'data:image/png;base64, '
	},

	cold: {
		name: 'Cold',
		array: [
			1,0,0,0,0,
			0,1,0,0,0,
			-0.2, 0.2, 0.1, 0.4, 0,
			0,0,0,1,0
		],
		preview: 'data:image/png;base64, '
	},

	black_white: {
		name: 'Black & White',
		array: [
			0,1,0,0,0,
			0,1,0,0,0,
			0,1,0,0,0,
			0,1,0,1,0
		],
		preview: 'data:image/png;base64, '
	},

	old: {
		name: 'Old',
		array: [
			1,0,0,0,0,
			-0.4,1.3,-0.4,0.2,-0.1,
			0,0,1,0,0,
			0,0,0,1,0
		],
		preview: 'data:image/png;base64, '
	},

	milk: {
		name: 'Milk',
		array: [
			0,1,0,0,0,
			0,1,0,0,0,
			0,0.6,1,0,0,
			0,0,0,1,0
		],
		preview: 'data:image/png;base64, '
	},

	purple: {
		name: 'Purple',
		array: [
			1,-0.2,0,0,0,
			0,1,0,-0.1,0,
			0,1.2,1,0.1,0,
			0,0,1.7,1,0
		],
		preview: 'data:image/png;base64, '
	},

	yellow: {
		name: 'Yellow',
		array: [
			1,0,0,0,0,
			-0.2,1,0.3,0.1,0,
			-0.1,0,1,0,0,
			0,0,0,1,0
		],
		preview: 'data:image/png;base64, '
	},

	vintage: {
		name: 'Vintage',
		array: [
			0.6279345635605994, 0.3202183420819367, -0.03965408211312453, 0, 9.651285835294123,
            0.02578397704808868, 0.6441188644374771, 0.03259127616149294, 0, 7.462829176470591,
            0.0466055556782719, -0.0851232987247891, 0.5241648018700465, 0, 5.159190588235296,
            0, 0, 0, 1, 0
		],
		preview: 'data:image/png;base64, '
	},

	kodachrome: {
		name: 'Kodachrome',
		array: [
			1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502,
            -0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203,
            -0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946,
            0, 0, 0, 1, 0
		],
		preview: 'data:image/png;base64, '
	},

	technicolor: {
		name: 'Technicolor',
		array: [
			1.9125277891456083, -0.8545344976951645, -0.09155508482755585, 0, 11.793603434377337,
            -0.3087833385928097, 1.7658908555458428, -0.10601743074722245, 0, -70.35205161461398,
            -0.231103377548616, -0.7501899197440212, 1.847597816108189, 0, 30.950940869491138,
            0, 0, 0, 1, 0
		],
		preview: 'data:image/png;base64, '
	},

	monochrome: {
		name: 'Monochrome',
		array: [
			0.95,0,0,0,0.05,
			0.85,0,0,0,0.15,
			0.5,0,0,0,0.5,
			0,0,0,1,0
		],
		preview: 'data:image/png;base64, '
	},

};

var FPDToolbarSmart = function($uiElementToolbar, fpdInstance) {

	'use strict';

	$ = jQuery;

	var instance = this,
		$body = $('body'),
		$uiToolbarSub = $uiElementToolbar.children('.fpd-sub-panel'),
		$colorPicker = $uiElementToolbar.find('.fpd-color-wrapper'),
		$window = $(window),
		colorDragging = false,
		resetScroll = true;

	this.isTransforming = false; //is true, while transforming via slider
	this.placement = fpdInstance.mainOptions.toolbarPlacement;

	var _initialize = function() {

		$uiElementToolbar.data('instance', instance).addClass('fpd-theme-'+fpdInstance.mainOptions.toolbarTheme);

		instance.setPlacement(fpdInstance.mainOptions.toolbarPlacement);

		//disable page scroll for touch devices
		if($uiElementToolbar.length > 0) {
			$uiElementToolbar.get(0).addEventListener('touchmove', function(evt) {

				if(evt.target.dataset.control !== 'text') {
					evt.preventDefault();
				}

			}, false);
		}

		$uiElementToolbar.children('.fpd-scroll-area').mCustomScrollbar({
			axis: 'x',
			scrollInertia: 200,
			mouseWheel: {
				enable: true
			},
			advanced:{
	        	autoExpandHorizontalScroll:true
	      	}
		});

		$body.on('mousedown touchstart', function(evt) {

			if($(evt.target).parents('.fpd-range-slider').length > 0) {
				instance.isTransforming = true;
			}

		})
		.on('mouseup touchend', function() {
			instance.isTransforming = false;
		});

		//close toolbar
		$uiElementToolbar.on('click touchend', '.fpd-close-panel', function(evt) {

			evt.stopPropagation();
			evt.preventDefault();

			$uiElementToolbar.find('.fpd-panel-font-family input').val('').keyup();
			$colorPicker.children('input').spectrum('hide');

			$uiToolbarSub.hide();

			if(fpdInstance.currentElement && fpdInstance.currentElement.exitEditing === 'function') {
				fpdInstance.currentElement.exitEditing();
			}

			fpdInstance.$container.trigger('toolbarPanelClose');

		});

		//set max values
		var maxValuesKeys = Object.keys(fpdInstance.mainOptions.maxValues);
		for(var i=0; i < maxValuesKeys.length; ++i) {

			var maxValueProp = maxValuesKeys[i];
			$uiElementToolbar.find('[data-control="'+maxValueProp+'"]').attr('max', fpdInstance.mainOptions.maxValues[maxValueProp]);

		}

		//first-level tools
		$uiElementToolbar.find('[class^="fpd-tool-"]').click(function() {

			var $this = $(this);

			if($this.data('panel')) { //has a sub a panel

				$this.tooltipster('hide');

				$uiToolbarSub.show() //display sub wrapper, if opener is active
				.children().removeClass('fpd-active') //hide all panels in sub wrapper
				.filter('.fpd-panel-'+$this.data('panel')).addClass('fpd-active'); //display related panel

				$uiToolbarSub.find('.fpd-slider-range').rangeslider('update', false, false);

				var element = fpdInstance.currentElement;

				if($this.data('panel') == 'color') {

					if(FPDUtil.elementHasColorSelection(element)) {

						var availableColors;
						if(element.__editorMode) {
							availableColors = ['#000'];
						}
						else if(element.colorLinkGroup) {
							availableColors = fpdInstance.colorLinkGroups[element.colorLinkGroup].colors;
						}
						else {
							availableColors = element.colors;
						}

						$colorPicker.children('input').spectrum('destroy');
						$colorPicker.empty().removeClass('fpd-colorpicker-group');

						//svg with more than one path
						if(element.type == FPDPathGroupName && (element.getObjects().length > 1 || availableColors == 1 || element.__editorMode)) {

							var paths = element.getObjects();
							for(var i=0; i < paths.length; ++i) {
								var path = paths[i],
									color = tinycolor(path.fill);

								$colorPicker.append('<input type="text" value="'+color.toHexString()+'" />');
							}

							$colorPicker.addClass('fpd-colorpicker-group').children('input').spectrum('destroy').spectrum({
								showPaletteOnly: $.isArray(element.colors) && !element.__editorMode,
								preferredFormat: "hex",
								showInput: true,
								showInitial: true,
								showButtons: false,
								showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
								palette: $.isArray(element.colors) ? element.colors : fpdInstance.mainOptions.colorPickerPalette,
								containerClassName: 'fpd-theme-'+fpdInstance.mainOptions.toolbarTheme,
								show: function(color) {

									var svgColors = FPDUtil.changePathColor(
										fpdInstance.currentElement,
										$colorPicker.children('input').index(this),
										color
									);

									FPDUtil.spectrumColorNames($(this).spectrum('container'), fpdInstance);

									element._tempFill = svgColors;

								},
								move: function(color) {

									var svgColors = FPDUtil.changePathColor(
										element,
										$colorPicker.children('input').index(this),
										color
									);

									fpdInstance.currentViewInstance.setElementParameters({fill: svgColors}, element);

								},
								change: function(color) {

									var svgColors = FPDUtil.changePathColor(
										element,
										$colorPicker.children('input').index(this),
										color
									);

									$(document).unbind("click.spectrum"); //fix, otherwise change is fired on every click
									fpdInstance.currentViewInstance.setElementParameters({fill: svgColors}, element);

								}
							});

						}
						//color list or for svg with one path
						else if(availableColors.length > 1 || (element.type == FPDPathGroupName && element.getObjects().length === 1)) {

							$colorPicker.html('<div class="fpd-scroll-area"><div class="fpd-color-palette fpd-grid"></div></div>');

							for(var i=0; i < availableColors.length; ++i) {

								var color = availableColors[i],
									colorName = fpdInstance.mainOptions.hexNames[color.replace('#', '').toLowerCase()];

								colorName = colorName ? colorName : color;
								$colorPicker.find('.fpd-grid').append('<div class="fpd-item fpd-tooltip" title="'+colorName+'" style="background-color: '+color+';"></div>')
								.children('.fpd-item:last').click(function() {

									var color = tinycolor($(this).css('backgroundColor'));

									$uiElementToolbar.find('.fpd-tool-color').css('background', color.toHexString());

									var fillValue = color.toHexString();
									if(fpdInstance.currentElement.type == FPDPathGroupName) {

										fillValue = FPDUtil.changePathColor(
											element,
											0,
											color
										);

									}

									fpdInstance.currentViewInstance.setElementParameters({fill: fillValue});

								});

							}

							FPDUtil.updateTooltip();
							$colorPicker.children('.fpd-scroll-area').mCustomScrollbar({
								scrollInertia: 200,
								documentTouchScroll: false,
								contentTouchScroll: true,
								mouseWheel: {
									enable: true,
									preventDefault: true
								},
							});

						}
						//colorwheel
						else {

							$colorPicker.html('<input type="text" value="'+(element.fill ? element.fill : availableColors[0])+'" />');

							$colorPicker.children('input').spectrum({
								flat: true,
								preferredFormat: "hex",
								showInput: true,
								showInitial: true,
								showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
								palette: fpdInstance.mainOptions.colorPickerPalette,
								containerClassName: 'fpd-theme-'+fpdInstance.mainOptions.toolbarTheme,
								allowEmpty: Boolean(element.__editorMode),
								show: function(color) {

									FPDUtil.spectrumColorNames($(this).spectrum('container').next('.sp-container'), fpdInstance);
									element._tempFill = color.toHexString();

								},
								move: function(color) {

									//only non-png images are changing while dragging
									if(colorDragging === false || FPDUtil.elementIsColorizable(element) !== 'png') {
										_setElementColor(color ? color.toHexString() : '');
									}

								},
								change: function(color) {

									$(document).unbind("click.spectrum"); //fix, otherwise change is fired on every click
									fpdInstance.currentViewInstance.setElementParameters({fill: color ? color.toHexString() : false}, element);

								}
							})
							.on('dragstart.spectrum', function() {
								colorDragging = true;
							})
							.on('dragstop.spectrum', function(evt, color) {
								colorDragging = false;
								_setElementColor(color.toHexString());
							});

						}
					}

					//patterns
					if((FPDUtil.isSVG(element) || FPDUtil.getType(element.type) === 'text') && element.patterns && element.patterns.length) {

						$uiToolbarSub.find('.fpd-tool-patterns .fpd-grid').empty();
						for(var i=0; i < element.patterns.length; ++i) {

							var patternUrl = element.patterns[i],
								$lastItem = $('<div/>', {
									'class': 'fpd-item',
									'data-pattern': patternUrl,
									'html': '<picture style="background-image: url('+patternUrl+');"></picture>'
								}).appendTo($uiToolbarSub.find('.fpd-tool-patterns .fpd-grid'));

							$lastItem.click(function() {

								var patternUrl = $(this).data('pattern');
								$uiElementToolbar.find('.fpd-tool-color').css('background', 'url('+patternUrl+')');
								fpdInstance.currentViewInstance.setElementParameters( {pattern: patternUrl} );


							});

						}

						$uiToolbarSub.find('.fpd-tool-patterns .fpd-scroll-area').mCustomScrollbar({
							scrollInertia: 200,
							documentTouchScroll: false,
							contentTouchScroll: true,
							mouseWheel: {
								enable: true,
								preventDefault: true
							},
						});

					}

					//stroke color
				    $uiToolbarSub.find('.fpd-stroke-color-picker input').spectrum('destroy').spectrum({
					    color: element.stroke ? element.stroke : '#000',
						flat: true,
						preferredFormat: "hex",
						showInput: true,
						showInitial: true,
						palette: element.strokeColors,
						showPaletteOnly: element.strokeColors && element.strokeColors.length > 0,
						containerClassName: 'fpd-theme-'+fpdInstance.mainOptions.toolbarTheme,
						move: function(color) {
							instance.isTransforming = true;
							fpdInstance.currentViewInstance.setElementParameters( {stroke: color.toHexString()} );

						},
						change: function(color) {

							fpdInstance.currentViewInstance.setElementParameters({stroke: color.toHexString()});

						}
					});


					//shadow color
				    $uiToolbarSub.find('.fpd-shadow-color-picker input').spectrum('destroy').spectrum({
					    color: element.stroke ? element.stroke : '#000',
						flat: true,
						preferredFormat: "hex",
						showInput: true,
						showInitial: true,
						allowEmpty: true,
						showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
						palette: fpdInstance.mainOptions.colorPickerPalette,
						containerClassName: 'fpd-theme-'+fpdInstance.mainOptions.toolbarTheme,
						move: function(color) {

							if(color) {
								instance.isTransforming = true;
								fpdInstance.currentViewInstance.setElementParameters( {shadowColor: color.toHexString()} );
							}

						},
						change: function(color) {
							fpdInstance.currentViewInstance.setElementParameters({shadowColor: color ? color.toHexString() : ''});

						}
					});

				}

				$uiElementToolbar.find('.fpd-panel-font-family .fpd-fonts-list').mCustomScrollbar({
					axis: 'y',
					scrollInertia: 200,
					documentTouchScroll: false,
					contentTouchScroll: true,
					mouseWheel: {
						enable: true,
						preventDefault: true
					},
					advanced:{
			        	autoExpandHorizontalScroll:true
			      	}
				});

				instance.updatePosition(fpdInstance.currentElement);

				fpdInstance.$container.trigger('toolbarPanelOpen', [$this.data('panel')]);

			}

		});

		//call content in tab
		$uiToolbarSub.find('.fpd-panel-tabs > span').click(function() {

			var $this = $(this);

			$this.addClass('fpd-active').siblings().removeClass('fpd-active');
			var $activePanel = $this.parent().siblings('.fpd-panel-tabs-content').children('[data-id="'+this.dataset.tab+'"]').addClass('fpd-active')

			$activePanel.siblings().removeClass('fpd-active');
			$activePanel.find('.fpd-slider-range').rangeslider('update', true, false);

			instance.updatePosition(fpdInstance.currentElement);

		});

		//create range slider
		var saveUndo = true;
		$uiToolbarSub.find('.fpd-slider-range').rangeslider({
			polyfill: false,
			rangeClass: 'fpd-range-slider',
			disabledClass: 'fpd-range-slider--disabled',
			horizontalClass: 'fpd-range-slider--horizontal',
		    verticalClass: 'fpd-range-slider--vertical',
		    fillClass: 'fpd-range-slider__fill',
		    handleClass: 'fpd-range-slider__handle',
		    onSlide: function(pos, value) {

			    instance.isTransforming = true;

			    if(fpdInstance.currentViewInstance && fpdInstance.currentViewInstance.currentElement) {

				    if(!this.$element.is(':visible')) {
					    return;
				    }

				    var props = {},
				    	propKey = this.$element.data('control');

				    props[propKey] = value;

				     //proportional scaling
				    if(propKey === 'scaleX' && fpdInstance.currentElement && fpdInstance.currentElement.lockUniScaling) {
					    props.scaleY = value;
				    }

					//fix: text vanished when autoselected
				    if((propKey == 'scaleX' && value == 0) || (propKey == 'scaleY' && value == 0)) {
					    return;
				    }

				    fpdInstance.currentViewInstance.setElementParameters(
						props,
						fpdInstance.currentViewInstance.currentElement,
						saveUndo
					);

					this.$element.parent().siblings('.fpd-slider-number').val(value);
					saveUndo = false;

			    }

		    },
		    onSlideEnd: function(pos, value) {

			    if(this.$element.data('control') === 'scaleX' && fpdInstance.currentElement && fpdInstance.currentElement.lockUniScaling) {
					$uiElementToolbar.find('[data-control="scaleY"]').val(value)
					.filter('.fpd-slider-range').rangeslider('update', true, false);
				}

			    instance.isTransforming = false;
			    saveUndo = true;
		    }

		});

		//Button with mulitple options
		$uiElementToolbar.on('click', '.fpd-btn-options', function(evt) {

			evt.preventDefault();

			var $this = $(this),
				options = $this.data('options'),
				optionKeys = Object.keys(options),
				currentVal = fpdInstance.currentElement ? fpdInstance.currentElement[this.dataset.control] : optionKeys[0],
				nextOption = optionKeys.indexOf(currentVal) == optionKeys.length - 1 ? optionKeys[0] : optionKeys[optionKeys.indexOf(currentVal)+1],
				params = {};

			params[this.dataset.control] = nextOption;
			$this.children('span').removeClass().addClass(options[nextOption]);
			fpdInstance.currentViewInstance.setElementParameters(params);


		});

		$uiElementToolbar.find('.fpd-toggle').click(function() {

			var $this = $(this).toggleClass('fpd-enabled'),
				toggleParameters = {};

			//ignore curved text switcher
			if(!$this.hasClass('fpd-curved-text-switcher')) {

				toggleParameters[$this.data('control')] = $this.hasClass('fpd-enabled') ? $this.data('enabled') : $this.data('disabled');

				if($this.hasClass('fpd-tool-uniscaling-locker')) {
					_lockUniScaling($this.hasClass('fpd-enabled'));
				}

				fpdInstance.currentViewInstance.setElementParameters(toggleParameters);

			}

		});

		$uiElementToolbar.find('.fpd-number').change(function() {

			var $this = $(this),
				numberParameters = {};

			if( this.value > Number($this.attr('max')) ) {
				this.value = Number($this.attr('max'));
			}

			if( this.value < Number($this.attr('min')) ) {
				this.value = Number($this.attr('min'));
			}

			var value = Number(this.value);

			if($this.hasClass('fpd-slider-number')) {

				$this.siblings('.fpd-range-wrapper').children('input').val(this.value)
				.rangeslider('update', true, false);

				if($this.data('control') === 'scaleX' && fpdInstance.currentElement && fpdInstance.currentElement.lockUniScaling) {
					$uiElementToolbar.find('[data-control="scaleY"]').val(value).change();
				}

			}

			numberParameters[$this.data('control')] = value;

			if(fpdInstance.currentViewInstance && $(document.activeElement).is(':not(textarea)')) {

				fpdInstance.currentViewInstance.setElementParameters(
					numberParameters,
					fpdInstance.currentViewInstance.currentElement
				);

			}

		});


		//append fonts to dropdown
		if(fpdInstance.mainOptions.fonts && fpdInstance.mainOptions.fonts.length > 0) {

			var $fontsList =  $uiToolbarSub.find('.fpd-fonts-list');
			for(var i=0; i < fpdInstance.mainOptions.fonts.length; ++i) {

				var font = fpdInstance.mainOptions.fonts[i],
					fontName = font;

				if(typeof font == 'object') {
					fontName = font.name;
				}

				$('<span/>', {
					'class': 'fpd-item',
					'data-value': fontName,
					'html': fontName,
					'css': {'fontFamily': fontName}
				}).appendTo($fontsList);

			}

			$uiElementToolbar
			.on('keyup', '.fpd-panel-font-family input', function() {

				var $items = $(this).css('font-family', 'Helvetica').nextAll('.fpd-fonts-list')
				.find('.fpd-item').hide();

				if(this.value.length === 0) {
					$items.show();
				}
				else {
					$items.filter(':containsCaseInsensitive("'+this.value+'")').show();
				}

			})
			.on('click', '.fpd-fonts-list .fpd-item', function() {

				var selectedFont = this.dataset.value;

				$uiElementToolbar.find('.fpd-tool-font-family .fpd-current-val').text(selectedFont).css('fontFamily', selectedFont);
				fpdInstance.currentViewInstance.setElementParameters({fontFamily: selectedFont});

			});

		}

		//Edit Text Tools
		$uiElementToolbar.on('click', '.fpd-tool-edit-text', function() {

			var val = $uiToolbarSub.find('.fpd-panel-edit-text textarea').val();
			$uiToolbarSub.find('.fpd-panel-edit-text textarea').focus().val('').val(val);

		});

	    $uiElementToolbar.find('textarea[data-control="text"]')
	    .on('keyup', function(evt) {

		    evt.stopPropagation;
		    evt.preventDefault();

		    var selectionStart = this.selectionStart,
			 	selectionEnd = this.selectionEnd;

		    fpdInstance.currentViewInstance.setElementParameters( {text: this.value} );

		    this.selectionStart = selectionStart;
			this.selectionEnd = selectionEnd;

	    });

		//advanced editing: crop, filters, color manipulation
		$uiElementToolbar.find('.fpd-tool-advanced-editing').click(function() {

			if(fpdInstance.currentViewInstance && fpdInstance.currentViewInstance.currentElement && fpdInstance.currentViewInstance.currentElement.source) {

				var source = fpdInstance.currentViewInstance.currentElement.source,
					$modal = FPDUtil.showModal($(fpdInstance.translatedUI).children('.fpd-image-editor-container').clone(), true),
					imageEditor = new FPDImageEditor(
						$modal.find('.fpd-image-editor-container'),
						fpdInstance.currentViewInstance.currentElement,
						fpdInstance
					);

				imageEditor.loadImage(source);

			}

		});

		//Position Tools
		$uiToolbarSub.on('click', '.fpd-panel-position.fpd-icon-button-group > span', function() {

			var $this = $(this);
			if($this.hasClass('fpd-align-left')) {
				fpdInstance.currentViewInstance.alignElement('left');
			}
			else if($this.hasClass('fpd-align-top')) {
				fpdInstance.currentViewInstance.alignElement('top');
			}
			else if($this.hasClass('fpd-align-right')) {
				fpdInstance.currentViewInstance.alignElement('right');
			}
			else if($this.hasClass('fpd-align-bottom')) {
				fpdInstance.currentViewInstance.alignElement('bottom');
			}
			else if($this.hasClass('fpd-align-center-h')) {
				fpdInstance.currentViewInstance.centerElement(true, false);
			}
			else if($this.hasClass('fpd-align-center-v')) {
				fpdInstance.currentViewInstance.centerElement(false, true);
			}
			else if($this.hasClass('fpd-flip-h')) {
				fpdInstance.currentViewInstance.setElementParameters({flipX: !fpdInstance.currentElement.get('flipX')});
			}
			else if($this.hasClass('fpd-flip-v')) {
				fpdInstance.currentViewInstance.setElementParameters({flipY: !fpdInstance.currentElement.get('flipY')});
			}

			instance.updatePosition(fpdInstance.currentElement);

		});

		//move layer position
		$uiElementToolbar.find('.fpd-tool-move-up, .fpd-tool-move-down').click(function() {

			var currentZ = fpdInstance.currentViewInstance.getZIndex();

			currentZ = $(this).hasClass('fpd-tool-move-up') ? currentZ+1 : currentZ-1;
			currentZ = currentZ < 0 ? 0 : currentZ;

			fpdInstance.currentViewInstance.setElementParameters( {z: currentZ} );

	    });

		//reset element
	    $uiElementToolbar.find('.fpd-tool-reset').click(function() {

		    $(document).unbind("click.spectrum"); //needs to be triggered, otherwise color is not resetted
			$uiElementToolbar.find('.tooltipstered').tooltipster('destroy');

			var originParams = fpdInstance.currentElement.originParams;
			delete originParams['clipPath'];

			//if element has bounding box, rescale for scale mode
			if(fpdInstance.currentElement.boundingBox) {
				fpdInstance.currentElement.scaleX = 1;
				originParams.boundingBox = fpdInstance.currentElement.boundingBox;
			}

		    fpdInstance.currentViewInstance.setElementParameters( fpdInstance.currentElement.originParams );
		    fpdInstance.currentViewInstance.deselectElement();

			FPDUtil.updateTooltip();

		});

		fpdInstance.$container.on('elementModify', function(evt, element, parameters) {

			if(parameters.fontSize) {
				$uiElementToolbar.find('.fpd-tool-text-size > .fpd-current-val').text(parseInt (parameters.fontSize));
			}

		});

	};

	var _toggleUiTool = function(tool, showHide) {

		showHide = showHide === undefined ? true : showHide;
		return $uiElementToolbar.find('.fpd-tool-'+tool).toggleClass('fpd-hidden', !showHide);

	};

	var _toggleSubTool = function(panel, tool, showHide) {

		showHide = Boolean(showHide);

		return $uiToolbarSub.children('.fpd-panel-'+panel)
		.children('.fpd-tool-'+tool).toggleClass('fpd-hidden', !showHide);

	};

	var _togglePanelTab = function(panel, tab, showHide) {

		$uiToolbarSub.children('.fpd-panel-'+panel)
		.find('.fpd-panel-tabs [data-tab="'+tab+'"]').toggleClass('fpd-disabled', !showHide);

	};

	var _setElementColor = function(color) {

		$uiElementToolbar.find('.fpd-tool-color').css('background', color);
		fpdInstance.currentViewInstance.changeColor(fpdInstance.currentViewInstance.currentElement, color);

	};

	var _lockUniScaling = function(toggle) {

		 $uiToolbarSub.find('.fpd-tool-uniscaling-locker > span').removeClass().addClass(toggle ? 'fpd-icon-locked' : 'fpd-icon-unlocked');
		 $uiToolbarSub.find('.fpd-tool-scaleY').toggleClass('fpd-disabled', toggle);

	};

	this.update = function(element) {

		this.hideTools();
		$uiElementToolbar.removeClass('fpd-type-image');

		_toggleUiTool('reset');

		var source = element.source,
			allowedImageTypes = [
				'png',
				'jpg',
				'jpeg',
				'svg'
			];

		if(source) {
			source = source.split('?')[0];//remove all url parameters
			var imageParts = source.split('.'),
				sourceExt = imageParts.pop().toLowerCase();

		}

		if(element.advancedEditing && source &&
			(FPDUtil.isSVG(element) || $.inArray(sourceExt, allowedImageTypes) !== -1 || sourceExt.search(/data:image\/(jpeg|png);/) !== -1)) {
			_toggleUiTool('advanced-editing');
		}

		//colors array, true=svg colorization
		if(FPDUtil.elementHasColorSelection(element)) {

			_toggleUiTool('color');
			_togglePanelTab('color', 'fill', true);
			$colorPicker.removeClass('fpd-hidden');

		}

		var showScale = (element.resizable && FPDUtil.getType(element.type) === 'image') || element.uniScalingUnlockable || element.__editorMode;
		if(showScale || element.rotatable || element.__editorMode) {
			_toggleUiTool('transform');

			_toggleSubTool('transform', 'scale', showScale);
			//uni scaling tools
			_lockUniScaling(element.lockUniScaling);
			_toggleSubTool('transform', 'uniscaling-locker', (element.uniScalingUnlockable || element.__editorMode));
			_toggleSubTool('transform', 'angle', Boolean(element.rotatable || element.__editorMode));
		}

		if(element.draggable || element.__editorMode) {
			_toggleUiTool('position');
		}

		if(element.zChangeable || element.__editorMode) {
			_toggleUiTool('layer-depth');
		}


		if((FPDUtil.isSVG(element) || FPDUtil.getType(element.type) === 'text') && element.patterns && element.patterns.length) {

			_toggleUiTool('color');
			_togglePanelTab('color', 'fill', true);
			_toggleUiTool('patterns', true);

		}
		else {
			_toggleUiTool('patterns', false);
		}

		//text options
		if(FPDUtil.getType(element.type) === 'text' && (element.editable || element.__editorMode)) {

			_toggleUiTool('font-family');
			_toggleUiTool('text-size', Boolean(element.resizable || element.__editorMode));
			_toggleUiTool('text-line-height');
			_toggleUiTool('text-letter-spacing');
			_toggleUiTool('text-styles');
			_toggleUiTool('text-transform');
			_toggleUiTool('text-align');
			_togglePanelTab('color', 'stroke', true);
			_togglePanelTab('color', 'shadow', true);

			if(element.curvable && !element.textBox) {
				_toggleUiTool('curved-text');
			}

			$uiElementToolbar.find('textarea[data-control="text"]').val(element.get('text'));

			if(fpdInstance.mainOptions.toolbarPlacement == 'smart' && fpdInstance.mainOptions.toolbarTextareaPosition == 'top') {
				$uiElementToolbar.children('.fpd-panel-edit-text').toggleClass('fpd-hidden', element.textPlaceholder || element.numberPlaceholder);

			}
			else {
				//hide edit-text when element is used as placeholder for numbers&names
				_toggleUiTool('edit-text', !element.textPlaceholder && !element.numberPlaceholder);
			}

		}
		else {
			$uiElementToolbar.addClass('fpd-type-image');
		}

		//add no margin to last visible tool
		$uiElementToolbar.find('.fpd-top-tools, .fpd-bottom-tools')
		.children('div').removeClass('fpd-no-margin').filter(':visible:last').addClass('fpd-no-margin');

		//display only enabled tabs and when tabs length > 1
		$uiToolbarSub.find('.fpd-panel-tabs').each(function(index, panelTabs) {

			var $panelTabs = $(panelTabs);
			$panelTabs.toggle($panelTabs.children(':not(.fpd-disabled)').length > 1);
			$panelTabs.children(':not(.fpd-disabled):first').addClass('fpd-active').click();

		});

		//set UI value by selected element
		$uiElementToolbar.find('[data-control]').each(function(index, uiElement) {

			var $uiElement = $(uiElement),
				parameter = $uiElement.data('control');

			if($uiElement.hasClass('fpd-number')) {

				if(element[parameter] !== undefined) {

					var numVal = $uiElement.attr('step') && $uiElement.attr('step').length > 1 ? parseFloat(element[parameter]).toFixed(2) : parseInt(element[parameter]);
					$uiElement.val(numVal);

					if($uiElement.prev('.fpd-range-wrapper')) {

						if(parameter == 'fontSize') {
							$uiElement.prev('.fpd-range-wrapper').children('input')
							.attr('min', element.minFontSize)
							.attr('max', element.maxFontSize);
						}
						else if(parameter == 'scaleX' || parameter == 'scaleY') {
							$uiElement.prev('.fpd-range-wrapper').children('input')
							.attr('min', element.minScaleLimit)
						}

						$uiElement.prev('.fpd-range-wrapper').children('input').val(numVal)
						.rangeslider('update', true, false);

					}

				}

			}
			else if($uiElement.hasClass('fpd-toggle')) {

				$uiElement.toggleClass('fpd-enabled', element[parameter] === $uiElement.data('enabled'));

			}
			else if($uiElement.hasClass('fpd-btn-options')) {

				$uiElement.children('span').removeClass()
				.addClass($uiElement.data('options')[element[parameter]]);

			}
			else if($uiElement.hasClass('fpd-tool-color')) {

				$uiElement.css('background', FPDUtil.getBgCssFromElement(element));

			}
			else if(parameter == 'fontSize') {
				$uiElementToolbar.find('.fpd-tool-text-size > .fpd-current-val').text(parseInt(element[parameter]));
			}
			else if(parameter == 'fontFamily') {

				if(element[parameter] !== undefined) {
					$uiElementToolbar.find('.fpd-tool-font-family > .fpd-current-val').text(element[parameter])
					.css('font-family', element[parameter]);
				}

			}

		});

		$uiElementToolbar.children('.fpd-scroll-area').mCustomScrollbar('update');

		instance.updatePosition(element);

	};

	this.updateUIValue = function(tool, value) {

		var $UIController = $uiElementToolbar.find('[data-control="'+tool+'"]');

		$UIController.val(value);
		$UIController.filter('.fpd-slider-range').rangeslider('update', true, false);

	};

	this.hideTools = function() {

		$uiElementToolbar //hide row
		.find('.fpd-top-tools > div, .fpd-bottom-tools > div')
		.addClass('fpd-hidden').removeClass('fpd-active'); //hide tool in row

		$uiToolbarSub.hide() //hide sub toolbar
		.children().removeClass('fpd-active')//hide all sub panels in sub toolbar
		.find('.fpd-panel-tabs > span').addClass('fpd-disabled'); //disable all tabs

		//remove active tabs
		$uiToolbarSub.find('.fpd-panel-tabs-content, .fpd-panel-tabs').children().removeClass('fpd-active')

		$colorPicker.addClass('fpd-hidden');
		//$uiElementToolbar.find('.fpd-close-panel').click();

		//top textarea
		$uiElementToolbar.children('.fpd-panel-edit-text').addClass('fpd-hidden');

	};

	this.updatePosition = function(element, showHide) {

		showHide = typeof showHide === 'undefined' ? true : showHide;

		if(!element) {
			this.toggle(false);
			return;
		}

		var oCoords = element.oCoords,
			topOffset = oCoords.mb.y,
			designerOffset = fpdInstance.$productStage.offset();

		var elemBoundingRect = element.getBoundingRect(),
			designerTop = fpdInstance.mainOptions.modalMode ? parseInt(fpdInstance.$container.parents('.fpd-modal-product-designer:first > .fpd-modal-wrapper').css("padding-top")) : designerOffset.top,
			lowestY = elemBoundingRect.top + elemBoundingRect.height, //set always to lowest point of element (consider angle)
			offsetY = element.padding + element.cornerSize + designerTop; //position under element

		topOffset = lowestY + offsetY; //position above canvas

		//LIMITS
		topOffset = topOffset > fpdInstance.$productStage.height() + designerTop ? fpdInstance.$productStage.height() + designerTop + 5 : topOffset;//do not move under designer
		var viewportH = fpdInstance.mainOptions.modalMode ? fpdInstance.$container.parents('.fpd-modal-product-designer:first > .fpd-modal-wrapper')[0].scrollHeight : document.body.scrollHeight;

		topOffset = topOffset + $uiElementToolbar.children('.fpd-sub-panel').height() > viewportH ? designerTop + elemBoundingRect.top -  ($uiElementToolbar.children('.fpd-sub-panel').height() + element.padding + element.cornerSize): topOffset; //do not move outside of viewport

		var posLeft = designerOffset.left + oCoords.mb.x,
			halfWidth =  $uiElementToolbar.outerWidth() * .5;

		posLeft = posLeft < halfWidth ? halfWidth : posLeft; //move toolbar not left outside of document
		posLeft = posLeft > $(window).width() - halfWidth ? $(window).width() - halfWidth : posLeft; //move toolbar not right outside of document

		$uiElementToolbar.css({
			left: posLeft,
			top: topOffset
		});

		if(resetScroll && fpdInstance.mainOptions.toolbarPlacement == 'smart' && !fpdInstance.mainOptions.modalMode && FPDUtil.isMobile()) {

			setTimeout(function() {

				if($uiElementToolbar.offset().top < topOffset) {
					$(window).scrollTop($(document).scrollTop() + (topOffset - $uiElementToolbar.offset().top));
				}

				resetScroll = true;

			}, 500);

			resetScroll = false;

		}

		this.toggle(showHide, false);

	};

	this.toggle = function(showHide, reset) {

		reset = reset === undefined ? true : reset;

		if(!showHide && reset) {
			$colorPicker.spectrum('destroy');
		}

		showHide = $uiElementToolbar.find('.fpd-top-tools > div:visible, .fpd-bottom-tools > div:visible').length == 0 ? false : showHide;
		$uiElementToolbar.toggleClass('fpd-show', showHide);

	};

	this.setPlacement = function(placement) {

		instance.placement = placement;

		//remove fpd-toolbar-placement-* class
		$uiElementToolbar.removeClass (function (index, css) {
		    return (css.match (/(^|\s)fpd-toolbar-placement-\S+/g) || []).join(' ');
		});
		$uiElementToolbar.addClass('fpd-toolbar-placement-'+placement);

		if(['inside-bottom', 'inside-top'].indexOf(placement) !== -1) { //inside canvas

			$uiElementToolbar.appendTo(fpdInstance.$mainWrapper);
			$uiElementToolbar.children('.fpd-scroll-area').mCustomScrollbar('disable');

		}
		else {

			if(fpdInstance.$container.parents('.fpd-modal-product-designer').length > 0 && !fpdInstance.$container.hasClass('fpd-device-smartphone')) {
				$uiElementToolbar.appendTo(fpdInstance.$container.parents('.fpd-modal-product-designer:first > .fpd-modal-wrapper'));
			}
			else {
				$uiElementToolbar.appendTo(fpdInstance.mainOptions.toolbarDynamicContext);
			}

			$uiElementToolbar.children('.fpd-scroll-area').mCustomScrollbar('update');

		}

	};

	_initialize();

};

var FPDMainBar = function(fpdInstance, $mainBar, $modules, $draggableDialog) {

	'use strict';

	$ = jQuery;

	var instance = this,
		$body = $('body'),
		$nav = $mainBar.children('.fpd-navigation');

	this.currentModules = fpdInstance.mainOptions.mainBarModules;
	this.$selectedModule = null;
	this.$container = $mainBar;
	instance.$content = $('<div class="fpd-content"></div>');
	this.mainBarShowing = true;

	var _initialize = function() {

		$draggableDialog
		.addClass('fpd-grid-columns-'+fpdInstance.mainOptions.gridColumns)
		.toggleClass('fpd-hidden', fpdInstance.$container.hasClass('fpd-main-bar-container-enabled'));

		if(fpdInstance.mainOptions.uiTheme !== 'doyle' && fpdInstance.$container.hasClass('fpd-topbar') && !fpdInstance.$container.hasClass('fpd-main-bar-container-enabled') && fpdInstance.$container.filter('[class*="fpd-off-canvas-"]').length === 0) { //draggable dialog
			instance.setContentWrapper('draggable-dialog');
		}
		else {
			instance.setContentWrapper('sidebar');
		}


		if(fpdInstance.$container.filter('[class*="fpd-off-canvas-"]').length > 0) {

			var touchStart = 0,
				panX = 0,
				closeStartX = 0,
				$closeBtn = $mainBar.children('.fpd-close-off-canvas');

			instance.$content.on('touchstart', function(evt) {

				touchStart = evt.originalEvent.touches[0].pageX;
				closeStartX = parseInt($closeBtn.css(fpdInstance.$container.hasClass('fpd-off-canvas-left') ? 'left' : 'right'));

			})
			.on('touchmove', function(evt) {

				evt.preventDefault();

				var moveX = evt.originalEvent.touches[0].pageX;
					panX = touchStart-moveX,
					targetPos = fpdInstance.$container.hasClass('fpd-off-canvas-left') ? 'left' : 'right';

				panX = Math.abs(panX) < 0 ? 0 : Math.abs(panX);
				instance.$content.css(targetPos, -panX);
				$closeBtn.css(targetPos, closeStartX - panX);

			})
			.on('touchend', function(evt) {

				var targetPos = fpdInstance.$container.hasClass('fpd-off-canvas-left') ? 'left' : 'right';

				if(Math.abs(panX) > 100) {

					instance.toggleDialog(false);

				}
				else {
					instance.$content.css(targetPos, 0);
					$closeBtn.css(targetPos, closeStartX);
				}

				panX = 0;

			});

		}

		//close off-canvas
		$mainBar.on('click', '.fpd-close-off-canvas', function(evt) {

			evt.stopPropagation();

			$nav.children('div').removeClass('fpd-active');
			instance.toggleDialog(false);

		});

		var $dialogContainer = fpdInstance.mainOptions.modalMode ? $('.fpd-modal-product-designer') : $body;
		$dialogContainer.append($draggableDialog);

		$draggableDialog.draggable({
			handle: $draggableDialog.find('.fpd-dialog-head'),
			containment: $dialogContainer
		});

		//select module
		$nav.on('click', '> div', function(evt) {

			evt.stopPropagation();

			var $this = $(this);

			fpdInstance.deselectElement();

			if(fpdInstance.currentViewInstance) {
				fpdInstance.currentViewInstance.currentUploadZone = null;
			}

			instance.$content.find('.fpd-manage-layers-panel')
			.find('.fpd-current-color, .fpd-path-colorpicker').spectrum('destroy');

			if((fpdInstance.$container.hasClass('fpd-topbar')
				|| fpdInstance.mainOptions.uiTheme == 'doyle')
				&& $this.hasClass('fpd-active')
			) { //hide dialog when clicking on active nav item

				fpdInstance.$container.removeClass('fpd-module-visible')
				$this.removeClass('fpd-active');
				instance.toggleDialog(false);

			}
			else {
				instance.callModule($this.data('module'), $this.data('dynamic-designs-id'));
			}

		});

		//prevent document scrolling when in dialog content
		$draggableDialog.on('mousewheel', function(evt) {
			 evt.preventDefault();
		});

		//nav in upload zones (text, images, designs)
		instance.$content.on('click', '.fpd-bottom-nav > div', function() {

			var $this = $(this);

			$this.addClass('fpd-active').siblings().removeClass('fpd-active');

			var $selectedModule = $this.parent().next().children('[data-module="'+$this.data('module')+'"]').addClass('fpd-active');
			$selectedModule.siblings().removeClass('fpd-active');

			//short timeout, because fpd-grid must be visible
			setTimeout(function() {
				FPDUtil.refreshLazyLoad($selectedModule.find('.fpd-grid'), false);
			}, 10);


		});

		//close dialog
		$body.on('click touchend', '.fpd-close-dialog', function() {

			if(fpdInstance.currentViewInstance && fpdInstance.currentViewInstance.currentUploadZone) {
				fpdInstance.currentViewInstance.deselectElement();
			}
			instance.toggleDialog(false);

		});

		fpdInstance.$container.on('viewSelect', function() {

			if(instance.$selectedModule) {

				if(instance.$selectedModule.filter('[data-module="manage-layers"]').length > 0) {
					FPDLayersModule.createList(fpdInstance, instance.$selectedModule);
				}
				else if(instance.$selectedModule.filter('[data-module="text-layers"]').length > 0) {
					FPDTextLayersModule.createList(fpdInstance, instance.$selectedModule);
				}
				//PLUS
				else if(typeof FPDNamesNumbersModule !== 'undefined'
					&& instance.$selectedModule.filter('[data-module="names-numbers"]').length > 0) {
					FPDNamesNumbersModule.setup(fpdInstance, instance.$selectedModule);
				}

			}

			/**
		     * Gets fired as soon as the list with the layers has been updated. Is fired when a view is selected or an object has been added/removed.
		     *
		     * @event FancyProductDesigner#layersListUpdate
		     * @param {Event} event
		     */
			fpdInstance.$container.trigger('layersListUpdate');

		});

		fpdInstance.$container.on('fabricObject:added fabricObject:removed', function(evt, element) {

			if(fpdInstance.productCreated && !element._ignore) {

				if(instance.$selectedModule) {
					if(instance.$selectedModule.filter('[data-module="manage-layers"]').length > 0) {
						FPDLayersModule.createList(fpdInstance, instance.$selectedModule);
					}
					else if(instance.$selectedModule.filter('[data-module="text-layers"]').length > 0) {
						FPDTextLayersModule.createList(fpdInstance, instance.$selectedModule);
					}
				}

				fpdInstance.$container.trigger('layersListUpdate');

			}

		});

		instance.setup(instance.currentModules);

	}

	//call module by name
	this.callModule = function(name, dynamicDesignsId) {

		var $selectedNavItem = $nav.children('div').removeClass('fpd-active')
		.filter(dynamicDesignsId ? '[data-dynamic-designs-id="'+dynamicDesignsId+'"]' : '[data-module="'+name+'"]:not([data-dynamic-designs-id])')
		.addClass('fpd-active');

		instance.$selectedModule = instance.$content.children('div').removeClass('fpd-active')
		.filter(dynamicDesignsId ? '[data-dynamic-designs-id="'+dynamicDesignsId+'"]' : '[data-module="'+name+'"]:not([data-dynamic-designs-id])')
		.addClass('fpd-active');

		if(fpdInstance.mainOptions.uiTheme !== 'doyle' && instance.$content.parent('.fpd-draggable-dialog').length > 0) {

			if($draggableDialog.attr('style') === undefined || $draggableDialog.attr('style') === '') {

				var topOffset = fpdInstance.$productStage.offset().top;
				if($draggableDialog.parent('.fpd-modal-product-designer').length > 0) {
					topOffset = parseInt($draggableDialog.siblings('.fpd-modal-wrapper').css('paddingTop'))+ $mainBar.height();
				}

				$draggableDialog.css('top', topOffset);
			}

			$draggableDialog.addClass('fpd-active')
			.find('.fpd-dialog-title').text($selectedNavItem.find('.fpd-label').text());

		}

		if(name === 'text') {
			instance.$selectedModule.find('textarea').focus();
		}
		else if(name === 'manage-layers') {

			if(fpdInstance.productCreated) {
				FPDLayersModule.createList(fpdInstance, instance.$selectedModule);
			}

		}
		else if(name === 'text-layers') {

			if(fpdInstance.productCreated) {
				FPDTextLayersModule.createList(fpdInstance, instance.$selectedModule);
			}

		}
		else if(typeof FPDNamesNumbersModule !== 'undefined' && name === 'names-numbers') {

			if(fpdInstance.productCreated) {
				FPDNamesNumbersModule.setup(fpdInstance, instance.$selectedModule);
			}

		}

		instance.toggleDialog(true);
		FPDUtil.refreshLazyLoad(instance.$selectedModule.find('.fpd-grid'), false);

	};

	this.callSecondary = function(className) {

		instance.callModule('secondary');

		instance.$content.children('.fpd-secondary-module').children('.'+className).addClass('fpd-active')
		.siblings().removeClass('fpd-active');

		var label = null;
		if(className === 'fpd-upload-zone-adds-panel') {
			instance.$content.find('.fpd-upload-zone-adds-panel .fpd-bottom-nav > :not(.fpd-hidden)').first().click();
		}
		else if(className === 'fpd-saved-designs-panel') {
			label = fpdInstance.getTranslation('actions', 'load')
		}

		if(fpdInstance.mainOptions.uiTheme !== 'doyle' && instance.$content.parent('.fpd-draggable-dialog').length > 0 && label) {

			$draggableDialog.addClass('fpd-active')
			.find('.fpd-dialog-title').text(label);

		}

		fpdInstance.$container.trigger('secondaryModuleCalled', [className, instance.$content.children('.fpd-secondary-module').children('.fpd-active')]);

	};

	this.setContentWrapper = function(wrapper) {

		$draggableDialog.removeClass('fpd-active');

		if(wrapper === 'sidebar') {

			if($nav.children('.fpd-active').length === 0) {
				$nav.children().first().addClass('fpd-active');
			}

			instance.$content.appendTo($mainBar);

		}
		else if(wrapper === 'draggable-dialog') {

			instance.$content.appendTo($draggableDialog);
			$nav.removeClass('fpd-hidden');

		}

		//if only modules exist, select it and hide nav
		if(instance.currentModules.length <= 1 && !fpdInstance.$container.hasClass('fpd-topbar')) {
			$nav.addClass('fpd-hidden');
		}
		else {
			$nav.removeClass('fpd-hidden');
		}

		//toogle tooltips
		$nav.children().each(function(i, navItem) {

			var $navItem = $(navItem);
			$navItem.filter('.tooltipstered').tooltipster('destroy');
			if(fpdInstance.$container.hasClass('fpd-sidebar')) {
				$navItem.addClass('fpd-tooltip').attr('title', $navItem.children('.fpd-label').text());
			}
			else {
				$navItem.removeClass('fpd-tooltip').removeAttr('title');
			}

		});

		FPDUtil.updateTooltip($nav);

		$nav.children('.fpd-active').click();

	};

	this.toggleDialog = function(toggle) {

		toggle = typeof toggle === 'undefined' ? true : toggle;

		//top bar is enabled
		if(fpdInstance.mainOptions.uiTheme !== 'doyle' && fpdInstance.$container.hasClass('fpd-topbar') && fpdInstance.$container.filter('[class*="fpd-off-canvas-"]').length === 0) {

			$draggableDialog.toggleClass('fpd-active', toggle);

		}
		else {

			fpdInstance.$container.toggleClass('fpd-module-visible', toggle)
			.children('.fpd-content').toggleClass('fpd-active', toggle)

		}

		//off-canvas is enabled
		if(fpdInstance.$container.filter('[class*="fpd-off-canvas-"]').length > 0) {

			instance.$container.toggleClass('fpd-show', toggle)
			.children('.fpd-close-off-canvas').removeAttr('style');
			instance.$content.removeAttr('style')
			.height(fpdInstance.$mainWrapper.height());

			if($nav.children('div').length === 0) {
				instance.$content.css('top', 0);
			}
			else {
				instance.$content.css('top', $nav.height());
			}

			//deselect element when main bar is showing
			if(!toggle && instance.mainBarShowing) {
				fpdInstance.deselectElement();
			}

		}

		if(!toggle) {
			$nav.children('.fpd-active').removeClass('fpd-active');
		}

		instance.mainBarShowing = toggle;

	};

	this.toggleUploadZonePanel = function(toggle) {

		toggle = typeof toggle === 'undefined' ? true : toggle;

		//do nothing when custom image is loading
		if(fpdInstance._loadingCustomImage) {
			return;
		}

		if(toggle) {
			instance.callSecondary('fpd-upload-zone-adds-panel');
		}
		else {

			fpdInstance.currentViewInstance.currentUploadZone = null;
			instance.toggleDialog(false);

		}

	};

	this.toggleUploadZoneAdds = function(customAdds) {

		var $uploadZoneAddsPanel = instance.$content.find('.fpd-upload-zone-adds-panel');

		$uploadZoneAddsPanel.find('.fpd-add-image').toggleClass('fpd-hidden', !Boolean(customAdds.uploads));
		$uploadZoneAddsPanel.find('.fpd-add-text').toggleClass('fpd-hidden', !Boolean(customAdds.texts));
		$uploadZoneAddsPanel.find('.fpd-add-design').toggleClass('fpd-hidden', !Boolean(customAdds.designs));

		if(fpdInstance.currentElement.price) {
			$uploadZoneAddsPanel.find('[data-module="text"] .fpd-btn > .fpd-price')
			.html(' - '+fpdInstance.formatPrice(fpdInstance.currentElement.price));
		}
		else {
			$uploadZoneAddsPanel.find('[data-module="text"] .fpd-btn > .fpd-price').html('');
		}

		if(fpdInstance.UZmoduleInstance_designs) {
			fpdInstance.UZmoduleInstance_designs.toggleCategories();
		}

		//select first visible add panel
		$uploadZoneAddsPanel.find('.fpd-off-canvas-nav > :not(.fpd-hidden)').first().click();

	};

	this.setup = function(modules) {

		instance.currentModules = modules;

		var selectedModule = fpdInstance.mainOptions.initialActiveModule ? fpdInstance.mainOptions.initialActiveModule : '';

		//if only one modules exist, select it and hide nav
		if(instance.currentModules.length <= 1 && !fpdInstance.$container.hasClass('fpd-topbar')) {

			selectedModule = instance.currentModules[0] ? instance.currentModules[0] : '';
			$nav.addClass('fpd-hidden');

		}
		else if(fpdInstance.$container.hasClass('fpd-sidebar') && selectedModule == '') {

			selectedModule = $nav.children().first().data('module');

		}
		else {
			$nav.removeClass('fpd-hidden');
		}

		$nav.empty();
		instance.$content.empty();

		//add selected modules
		modules.forEach(function(module) {

			var moduleType = module,
				$moduleIcon = $('<span class="fpd-nav-icon"></span>'),
				navItemTitle = '',
				dynamicDesignId = null,
				moduleAttrs = '',
				useFpdIcon = true;

			if(module.search('designs') == 0) {

				moduleType = 'designs';

				if(!FPDUtil.isEmpty(fpdInstance.mainOptions.dynamicDesigns) && module.search('designs_') == 0) {

					dynamicDesignId = module.split('_').pop();

					if(dynamicDesignId && fpdInstance.mainOptions.dynamicDesigns[dynamicDesignId]) {

						var dynamicDesignConfig = fpdInstance.mainOptions.dynamicDesigns[dynamicDesignId];

						navItemTitle = dynamicDesignConfig.name;
						moduleAttrs += ' data-dynamic-designs-id="'+dynamicDesignId+'"';

						if(!FPDUtil.isEmpty(dynamicDesignConfig.icon) && dynamicDesignConfig.icon.indexOf('.svg') != -1) {

							useFpdIcon = false;

							$.get(dynamicDesignConfig.icon, function(data) {
								$moduleIcon.append($(data).children('svg'));
							});
						}

					}
					else { //dynamic designs module does not exist
						return;
					}
				}

			}

			var $module = $modules.children('[data-module="'+moduleType+'"]'),
				$moduleClone = $module.clone(),
				moduleInstance;

			if(!dynamicDesignId) {
				navItemTitle = $module.data('title');
			}

			if(useFpdIcon) {
				$moduleIcon.addClass($module.data('moduleicon'));
			}

			moduleAttrs += fpdInstance.$container.hasClass('fpd-sidebar') ? ' class="fpd-tooltip"' : '';
			moduleAttrs += fpdInstance.$container.hasClass('fpd-sidebar') ? ' title="'+navItemTitle+'"' : '';

			$nav.append('<div data-module="'+moduleType+'" '+moduleAttrs+'><span class="fpd-label">'+navItemTitle+'</span></div>');
			$nav.children('div:last').prepend($moduleIcon);

			instance.$content.append($moduleClone);

			if(moduleType === 'products') {
				moduleInstance = new FPDProductsModule(fpdInstance, $moduleClone);
			}
			else if(moduleType === 'text') {
				moduleInstance = new FPDTextModule(fpdInstance, $moduleClone);
			}
			else if(moduleType === 'designs') {
				moduleInstance = new FPDDesignsModule(fpdInstance, $moduleClone, dynamicDesignId);
			}
			else if(moduleType === 'images') {
				moduleInstance = new FPDImagesModule(fpdInstance, $moduleClone);
			}
			else if(moduleType === 'layouts') {
				moduleInstance = new FPDLayoutsModule(fpdInstance, $moduleClone);
			}
			//PLUS
			else if(typeof FPDDrawingModule !== 'undefined' && moduleType === 'drawing') {
				moduleInstance = new FPDDrawingModule(fpdInstance, $moduleClone);
			}
			else if(typeof FPDDynamicViews !== 'undefined' && moduleType === 'dynamic-views') {
				moduleInstance = new FPDDynamicViews(fpdInstance, $moduleClone);
			}

			if(moduleInstance) {
				fpdInstance['moduleInstance_'+module] = moduleInstance;
			}

		});

		if(instance.$content.children('[data-module="manage-layers"]').length === 0) {
			instance.$content.append($modules.children('[data-module="manage-layers"]').clone());
		}

		instance.$content.append($modules.children('[data-module="secondary"]').clone());

		//add upload zone modules
		var uploadZoneModules = ['images', 'text', 'designs'];
		for(var i=0; i < uploadZoneModules.length; ++i) {

			var module = uploadZoneModules[i],
				$module = $modules.children('[data-module="'+module+'"]'),
				$moduleClone = $module.clone(),
				moduleInstance;

			instance.$content.find('.fpd-upload-zone-content').append($moduleClone);

			if(module === 'text') {
				moduleInstance = new FPDTextModule(fpdInstance, $moduleClone);
			}
			else if(module === 'designs') {
				moduleInstance = new FPDDesignsModule(fpdInstance, $moduleClone);
			}
			else if(module === 'images') {
				moduleInstance = new FPDImagesModule(fpdInstance, $moduleClone);
			}

			if(moduleInstance) {
				fpdInstance['UZmoduleInstance_'+module] = moduleInstance;
			}

		}

		if(fpdInstance.$container.hasClass('fpd-device-desktop') || fpdInstance.$container.parents('.ui-composer-page').length) {
			$nav.children('[data-module="'+selectedModule+'"]').click();
		}

	};

	_initialize();

};

FPDMainBar.availableModules = [
	'products',
	'images',
	'text',
	'designs',
	'manage-layers',
	'text-layers',
	'layouts'
];

var FPDActions = function(fpdInstance, $actions) {

	'use strict';

	$ = jQuery;

	var instance = this,
		snapLinesGroup,
		downloadFilename = fpdInstance.mainOptions.downloadFilename;

	this.currentActions = fpdInstance.mainOptions.actions;

	var _initialize = function() {

		//add set action buttons
		if($actions) {

			instance.setup(instance.currentActions);

			//action click handler
			fpdInstance.$container.on('click', '.fpd-actions-wrapper .fpd-action-btn', function() {

				var $this = $(this);

				if($this.hasClass('tooltipstered')) {
					$this.tooltipster('hide');
				}

				instance.doAction($this);

				//doyle
				$this.parents('.fpd-dropdown-btn:first').removeClass('fpd-active')

			});

			//doyle
			fpdInstance.$container.on('click', '.fpd-actions-wrapper .fpd-dropdown-btn >  .fpd-label', function() {

				$(this).parent().toggleClass('fpd-active')
				.parents('.fpd-actions-wrapper:first').siblings().find('.fpd-dropdown-btn').removeClass('fpd-active')

			});

		}

		fpdInstance.$container.on('viewSelect', function(evt, viewIndex, viewInstance) {

			instance.resetAllActions();

			fpdInstance.$mainWrapper.find('[data-action="previous-view"], [data-action="next-view"]').toggleClass('fpd-hidden', fpdInstance.viewInstances.length <= 1);
			fpdInstance.$mainWrapper.find('[data-action="previous-view"]').toggleClass('fpd-disabled', viewIndex === 0);
			fpdInstance.$mainWrapper.find('[data-action="next-view"]').toggleClass('fpd-disabled', viewIndex === fpdInstance.viewInstances.length - 1);

		});

	};

	//set action button to specific position
	var _setActionButtons = function(pos) {

		fpdInstance.$container.find('.fpd-actions-container').append('<div class="fpd-actions-wrapper fpd-pos-'+pos+'"></div>');

		var posActions = instance.currentActions[pos];

		if(fpdInstance.mainOptions.uiTheme == 'doyle') {

			if(pos == 'left') {

				var $targetWrapper = fpdInstance.$container.find('.fpd-actions-wrapper.fpd-pos-'+pos).append('<div class="fpd-dropdown-btn"><span class="fpd-label">'+fpdInstance.getTranslation('actions', 'menu_file', 'File')+'</span><div class="fpd-dropdown-menu"></div></div>').find('.fpd-dropdown-menu');

				for(var i=0; i < posActions.length; ++i) {

					var actionName = posActions[i],
						$action = $actions.children('[data-action="'+actionName+'"]');

					$action = $action.clone().removeClass('fpd-tooltip');
					$action.append($action.attr('title'))

					$targetWrapper.append($action);
				}

			}

			if(pos == 'right') {

				var $targetWrapper = fpdInstance.$container.find('.fpd-actions-wrapper.fpd-pos-'+pos).append('<div class="fpd-dropdown-btn"><span class="fpd-label">'+fpdInstance.getTranslation('actions', 'menu_tools', 'Tools')+'</span><div class="fpd-dropdown-menu"></div></div>').find('.fpd-dropdown-menu');

				for(var i=0; i < posActions.length; ++i) {

					var actionName = posActions[i],
						$action = $actions.children('[data-action="'+actionName+'"]');

					$action = $action.clone().removeClass('fpd-tooltip');
					$action.append($action.attr('title'))

					$targetWrapper.append($action);
				}

			}

			if(pos == 'top') {

				var $targetWrapper = fpdInstance.$container.find('.fpd-actions-wrapper.fpd-pos-'+pos);

				for(var i=0; i < posActions.length; ++i) {

					var actionName = posActions[i],
						$action = $actions.children('[data-action="'+actionName+'"]');

					$action = $action.clone().removeClass('fpd-tooltip');
					$action.append('<span>'+$action.attr('title')+'</span>')

					$targetWrapper.append($action);
				}

			}

		}
		else {

			for(var i=0; i < posActions.length; ++i) {

				var actionName = posActions[i],
					$action = $actions.children('[data-action="'+actionName+'"]');

				fpdInstance.$container.find('.fpd-actions-wrapper.fpd-pos-'+pos).append($action.clone());
			}

		}

	};

	//returns an object with the saved products for the current showing product
	var _getSavedProducts = function() {

		return FPDUtil.localStorageAvailable() ? JSON.parse(window.localStorage.getItem(fpdInstance.$container.attr('id'))) : false;

	};

	//download png, jpeg or pdf
	this.downloadFile = function(type, onlyCurrentView) {

		if(!fpdInstance.currentViewInstance) { return; }

		onlyCurrentView = onlyCurrentView === undefined ? false : onlyCurrentView;

		if(type === 'jpeg' || type === 'png') {

			var a = document.createElement('a'),
				background = type === 'jpeg' ? '#fff' : 'transparent';

			if(typeof a.download !== 'undefined') {

				if(onlyCurrentView) {

					fpdInstance.currentViewInstance.toDataURL(function(dataURL) {

						download(dataURL, downloadFilename+'.'+type, 'image/'+type);

					}, background, {format: type}, fpdInstance.watermarkImg);

				}
				else {

					fpdInstance.getProductDataURL(function(dataURL) {

						download(dataURL, downloadFilename+'.'+type, 'image/'+type);

					}, background, {format: type});

				}

			}
			else {
				fpdInstance.createImage(true, false, background, {format: type}, onlyCurrentView);
			}

		}
		else if(type === 'svg') {

			download(
				fpdInstance.currentViewInstance.toSVG({suppressPreamble: false}, null, false, fpdInstance.watermarkImg),
				'Product_'+fpdInstance.currentViewIndex+'.svg',
				'image/svg+xml'
			);

		}
		else {

			var _createPDF = function(dataURLs) {

				dataURLs = typeof dataURLs === 'string' ? [dataURLs] : dataURLs;

				var doc;
				for(var i=0; i < dataURLs.length; ++i) {

					var viewWidth = fpdInstance.viewInstances[i].options.stageWidth,
						viewHeight = fpdInstance.viewInstances[i].options.stageHeight,
						orien = viewWidth > viewHeight ? 'l' : 'p';

					if(i != 0) { //non-first pages
						doc.addPage([viewWidth, viewHeight], orien);
					}
					else { //first page
						doc = new jspdf.jsPDF({orientation: orien, unit: 'px', format: [viewWidth, viewHeight]})
					}

					doc.addImage(dataURLs[i], 'PNG', 0, 0, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight());

				}

				doc.save(downloadFilename+'.pdf');

			};

			onlyCurrentView ? fpdInstance.currentViewInstance.toDataURL(_createPDF, 'transparent', {format: 'png'}, fpdInstance.watermarkImg) : fpdInstance.getViewsDataURL(_createPDF, 'transparent', {format: 'png'});

		}

	};

	this.setup = function(actions) {

		this.currentActions = actions;

		fpdInstance.$container.find('.fpd-actions-container').empty();

		var keys = Object.keys(actions);
		for(var i=0; i < keys.length; ++i) {

			var posActions = actions[keys[i]];
			if(typeof posActions === 'object' && posActions.length > 0) {
				_setActionButtons(keys[i]);
			}

		}

	};

	this.doAction = function($this) {

		if(!fpdInstance.currentViewInstance) { return; }

		var action = $this.data('action');

		fpdInstance.deselectElement();

		if(action === 'print') {

			fpdInstance.print();

		}
		else if(action === 'reset-product') {

			var confirmReset = confirm(fpdInstance.getTranslation('misc', 'reset_confirm'));
			if(confirmReset) {
				fpdInstance.loadProduct(fpdInstance.currentViews);
			}

		}
		else if(action === 'undo') {

			fpdInstance.currentViewInstance.undo();

		}
		else if(action === 'redo') {

			fpdInstance.currentViewInstance.redo();

		}
		else if(action === 'info') {

			FPDUtil.showModal($this.children('.fpd-info-content').text(), false, '', fpdInstance.$modalContainer);

		}
		else if(action === 'preview-lightbox') {

			fpdInstance.getProductDataURL(function(dataURL) {

				var image = new Image();
				image.src = dataURL;

				image.onload = function() {
					FPDUtil.showModal('<div style="text-align: center;"><img src="'+this.src+'" download="product.png" /></div>', true);
				}

			});

		}
		else if(action === 'save') {

			fpdInstance.mainBar.toggleDialog(false);

			var $prompt = FPDUtil.showModal(fpdInstance.getTranslation('actions', 'save_placeholder'), false, 'prompt', fpdInstance.$modalContainer);
			$prompt.find('.fpd-btn').text(fpdInstance.getTranslation('actions', 'save')).click(function() {

				fpdInstance.doUnsavedAlert = false;

				var title = $(this).siblings('input:first').val(),
					scaling = FPDUtil.getScalingByDimesions(fpdInstance.currentViewInstance.options.stageWidth, fpdInstance.currentViewInstance.options.stageHeight, 300, 300, 'cover');

				fpdInstance.currentViewInstance.toDataURL(function(thumbnail) {

					//get key and value
					var product = fpdInstance.getProduct();

					if(product && fpdInstance.mainOptions.saveActionBrowserStorage) {

						//check if there is an existing products array
						var savedProducts = _getSavedProducts();
						if(!savedProducts) {
							//create new
							savedProducts = [];
						}

						savedProducts.push({thumbnail: thumbnail, product: product, title: title});
						window.localStorage.setItem(fpdInstance.$container.attr('id'), JSON.stringify(savedProducts));

						FPDUtil.showMessage(fpdInstance.getTranslation('misc', 'product_saved'));

					}

					$prompt.find('.fpd-modal-close').click();
					fpdInstance.$container.trigger('actionSave', [title, thumbnail, product]);

				}, 'transparent', {multiplier: scaling, format: 'png'});

			});

		}
		else if(action === 'load') {

			fpdInstance.mainBar.$content.find('.fpd-saved-designs-panel .fpd-grid').empty();

			//load all saved products into list
			if(fpdInstance.mainOptions.saveActionBrowserStorage) {

				var savedProducts = _getSavedProducts();
				if(savedProducts && savedProducts.length > 0) {

					for(var i=0; i < savedProducts.length; ++i) {

						var savedProduct = savedProducts[i];
						instance.addSavedProduct(savedProduct.thumbnail, savedProduct.product, savedProduct.title);

					}

					FPDUtil.createScrollbar(fpdInstance.mainBar.$content.find('.fpd-saved-designs-panel .fpd-scroll-area'));
					fpdInstance.mainBar.$content.find('.fpd-saved-designs-panel .fpd-scroll-area').mCustomScrollbar('update');

				}
				else {
					fpdInstance.mainBar.$content.find('.fpd-saved-designs-panel .fpd-empty-saved-designs').toggleClass('fpd-hidden', false);
				}

			}

			fpdInstance.$container.trigger('actionLoad');

			fpdInstance.mainBar.callSecondary('fpd-saved-designs-panel');


		}
		else if(action === 'manage-layers') {

			fpdInstance.mainBar.callModule('manage-layers');

		}
		else if(action === 'snap') {

			$this.toggleClass('fpd-active');
			fpdInstance.currentViewInstance._snapElements = $this.hasClass('fpd-active');

			fpdInstance.$mainWrapper.children('.fpd-snap-line-h, .fpd-snap-line-v').hide();

			if($this.hasClass('fpd-active')) {

				var lines = [],
					gridX = fpdInstance.mainOptions.snapGridSize[0] ? fpdInstance.mainOptions.snapGridSize[0] : 50,
					gridY = fpdInstance.mainOptions.snapGridSize[1] ? fpdInstance.mainOptions.snapGridSize[1] : 50,
					linesXNum = Math.ceil(fpdInstance.currentViewInstance.options.stageWidth / gridX),
					linesYNum = Math.ceil(fpdInstance.currentViewInstance.options.stageHeight / gridY);

				//add x-lines
				for(var i=0; i < linesXNum; ++i) {

					var lineX = new fabric.Rect({
						width: 1,
						height: fpdInstance.currentViewInstance.options.stageHeight,
						fill: '#ccc',
						opacity: 0.6,
						left: i * gridX,
						top: 0
					});

					lines.push(lineX);

				}

				//add y-lines
				for(var i=0; i < linesYNum; ++i) {

					var lineY = new fabric.Rect({
						width: fpdInstance.currentViewInstance.options.stageWidth,
						height: 1,
						fill: '#ccc',
						opacity: 0.6,
						top: i * gridY,
						left: 0
					});

					lines.push(lineY);

				}

				snapLinesGroup = new fabric.Group(lines, {id: '_snap_lines_group', left: 0, top: 0, evented: false, selectable: false, _ignore: true});
				fpdInstance.currentViewInstance.stage.add(snapLinesGroup);

			}
			else {

				if(snapLinesGroup) {
					fpdInstance.currentViewInstance.stage.remove(snapLinesGroup);
				}

			}

		}
		else if(action === 'qr-code') {

			var $internalModal = FPDUtil.showModal($this.children('.fpd-modal-context').clone(), false, '', fpdInstance.$modalContainer),
				$colorPickers = $internalModal.find('.fpd-qrcode-colors input').spectrum({
					preferredFormat: "hex",
					showInput: true,
					showInitial: true,
					showButtons: false,
					replacerClassName: 'fpd-spectrum-replacer',
				});

			$internalModal.find('.fpd-add-qr-code').click(function() {

				var text = $internalModal.find('.fpd-modal-context > input').val();

				if(text && text.length !== 0) {

					var $qrcodeWrapper = $internalModal.find('.fpd-qrcode-wrapper').empty(),
						qrcode = new QRCode($qrcodeWrapper.get(0), {
					    text: text,
					    width: 256,
					    height: 256,
					    colorDark : $colorPickers.filter('.fpd-qrcode-color-dark').spectrum('get').toHexString(),
					    colorLight : $colorPickers.filter('.fpd-qrcode-color-light').spectrum('get').toHexString(),
					    correctLevel : QRCode.CorrectLevel.H
					});

					$qrcodeWrapper.find('img').on('load', function() {

						fpdInstance.addElement(
							'image',
							this.src,
							'QR-Code - '+text,
							fpdInstance.currentViewInstance.options.qrCodeProps
						);

						$internalModal.find('.fpd-modal-close').click();

					});

				}

			});

			$internalModal.on('modalRemove', function() {
				$colorPickers.spectrum('destroy');
			});

		}
		else if(action === 'zoom') {

			if(!$this.hasClass('fpd-active')) {

				var $contextClone = $this.children('.fpd-action-context').clone();
				fpdInstance.$mainWrapper.append($contextClone);

				var startVal = fpdInstance.currentViewInstance.stage.getZoom() / fpdInstance.currentViewInstance.responsiveScale;

				$contextClone.find('.fpd-zoom-slider')
				.attr('step', fpdInstance.mainOptions.zoomStep).attr('max', fpdInstance.mainOptions.maxZoom)
				.val(startVal).rangeslider({
					polyfill: false,
					rangeClass: 'fpd-range-slider',
					disabledClass: 'fpd-range-slider--disabled',
					horizontalClass: 'fpd-range-slider--horizontal',
				    verticalClass: 'fpd-range-slider--vertical',
				    fillClass: 'fpd-range-slider__fill',
				    handleClass: 'fpd-range-slider__handle',
				    onSlide: function(pos, value) {
						fpdInstance.setZoom(value);
				    }
				});

				$contextClone.find('.fpd-stage-pan').click(function() {

					fpdInstance.currentViewInstance.dragStage = !fpdInstance.currentViewInstance.dragStage;
					fpdInstance.$productStage.toggleClass('fpd-drag');

				});

			}
			else {

				fpdInstance.currentViewInstance.dragStage = false;
				fpdInstance.$productStage.removeClass('fpd-drag');
				fpdInstance.$mainWrapper.children('.fpd-action-context').remove();

			}

			$this.toggleClass('fpd-active');

		}
		else if(action === 'download') {

			var $internalModal = FPDUtil.showModal($this.children('.fpd-modal-context').clone(), false, '', fpdInstance.$modalContainer);

			$internalModal.find('.fpd-modal-context span[data-value]').click(function() {

				var $this = $(this);

				instance.downloadFile($this.data('value'), $this.siblings('.fpd-checkbox:first').children('input').is(':checked'));
				$internalModal.find('.fpd-modal-close').click();

			});


		}
		else if(action === 'magnify-glass') {

			fpdInstance.resetZoom();

			if($this.hasClass('fpd-active')) {

				$(".fpd-zoom-image,.zoomContainer").remove();
				fpdInstance.$productStage.children('.fpd-view-stage').eq(fpdInstance.currentViewIndex).removeClass('fpd-hidden');

			}
			else {

				fpdInstance.toggleSpinner();

				var scaling = Number(2000 / fpdInstance.currentViewInstance.options.stageWidth).toFixed(2),
					dataURL = fpdInstance.currentViewInstance.stage.toDataURL({multiplier: scaling, format: 'png'});

				fpdInstance.$productStage.append('<img src="'+dataURL+'" class="fpd-zoom-image" />')
				.children('.fpd-zoom-image').elevateZoom({
					scrollZoom: true,
					borderSize: 1,
					zoomType: "lens",
					lensShape: "round",
					lensSize: 200,
					responsive: true,
					onZoomedImageLoaded: function($elem) {
						$('.zoomContainer').appendTo('.fpd-modal-product-designer .fpd-main-wrapper'); //set zoom container inside main wrapper in modal mode
						fpdInstance.toggleSpinner(false);
					}
				});


				fpdInstance.$productStage.children('.fpd-view-stage').addClass('fpd-hidden');

			}

			$this.toggleClass('fpd-active');

		}
		else if(action === 'ruler') {

			if($this.hasClass('fpd-active')) {

				var rulerHor = fpdInstance.currentViewInstance.getElementByID('_ruler_hor');
				if(rulerHor) {
					fpdInstance.currentViewInstance.stage.remove(rulerHor);
				}

				var rulerVer = fpdInstance.currentViewInstance.getElementByID('_ruler_ver');
				if(rulerVer) {
					fpdInstance.currentViewInstance.stage.remove(rulerVer);
				}

			}
			else {

				var pixelUnitsOptions = {
					fill: '#979797',
					fontSize: 10,
					fontFamily: 'Arial'
				};

				fabric.util.loadImage(FPDActions.rulerHorImg, function (img) {

					var groupRulerHor = new fabric.Group([], {
						left: 0,
						top: 0,
						originX: 'left',
						originY: 'top',
						evented: false,
						selectable: false,
						id: '_ruler_hor'
					});

					var rect = new fabric.Rect({
					    width: fpdInstance.currentViewInstance.options.stageWidth,
					    height: 30
					});

					rect.setPatternFill({
				        source: img,
				        repeat: 'repeat-x'
				    });

				    groupRulerHor.addWithUpdate(rect);

					var loopX = Math.ceil(fpdInstance.currentViewInstance.options.stageWidth / 100);
				    for(var i=1; i < loopX; ++i) {
					    var text = new fabric.Text(String(i*100), $.extend({}, pixelUnitsOptions, {top: 3, left: (i*100)+3}));
					    groupRulerHor.addWithUpdate(text);
					}

					fpdInstance.currentViewInstance.stage.add(groupRulerHor).renderAll();

				});

				fabric.util.loadImage(FPDActions.rulerVerImg, function (img) {

					var groupRulerVer = new fabric.Group([], {
						left: 0,
						top: 0,
						originX: 'left',
						originY: 'top',
						evented: false,
						selectable: false,
						id: '_ruler_ver'
					});

					var rect = new fabric.Rect({
					    width: 30,
					    height: fpdInstance.currentViewInstance.options.stageHeight
					});

					rect.setPatternFill({
				        source: img,
				        repeat: 'repeat-y'
				    });

				    groupRulerVer.addWithUpdate(rect);

					var loopX = Math.ceil(fpdInstance.currentViewInstance.options.stageWidth / 100);
				    for(var i=1; i < loopX; ++i) {
					    var text = new fabric.Text(String(i*100), $.extend({}, pixelUnitsOptions, {
						    top: (i*100)+3,
						    left: 12,
						    angle: 90,
						    originY: 'bottom'})
						);
					    groupRulerVer.addWithUpdate(text);
					}

					fpdInstance.currentViewInstance.stage.add(groupRulerVer).renderAll();

				});

			}

			$this.toggleClass('fpd-active');

		}
		else if(action === 'previous-view') {
			fpdInstance.selectView(fpdInstance.currentViewIndex - 1);
		}
		else if(action === 'next-view') {
			fpdInstance.selectView(fpdInstance.currentViewIndex + 1);
		}
		else if(action === 'guided-tour') {

			if(fpdInstance.mainOptions.guidedTour && Object.keys(fpdInstance.mainOptions.guidedTour).length > 0) {
				var firstKey = Object.keys(fpdInstance.mainOptions.guidedTour)[0];
				fpdInstance.selectGuidedTourStep(firstKey);
			}

		}

	};

	this.resetAllActions = function() {

		$(".fpd-zoom-image,.zoomContainer").remove();
		fpdInstance.$productStage.children('.fpd-view-stage').eq(fpdInstance.currentViewIndex).removeClass('fpd-hidden');

		fpdInstance.$mainWrapper.find('.fpd-action-btn').removeClass('fpd-active');

	};

	this.hideAllTooltips = function() {

		fpdInstance.$mainWrapper.find('.fpd-action-btn.tooltipstered').tooltipster('hide');

	};

	//add a saved product to the load dialog
	this.addSavedProduct = function(thumbnail, product, title) {

		title = title ? title : '';

		//create new list item
		var $gridWrapper = fpdInstance.mainBar.$content.find('.fpd-saved-designs-panel .fpd-grid'),
			htmlTitle = title !== '' ? 'title="'+title+'"' : '';

		$gridWrapper.append('<div class="fpd-item fpd-tooltip" '+htmlTitle+'><picture style="background-image: url('+thumbnail+')" ></picture><div class="fpd-remove-design"><span class="fpd-icon-remove"></span></div></div>')
		.children('.fpd-item:last').click(function(evt) {

			fpdInstance.loadProduct($(this).data('product'));
			fpdInstance.currentProductIndex = -1;

		}).data('product', product)
		.children('.fpd-remove-design').click(function(evt) {

			evt.stopPropagation();

			var $item = $(this).parent('.fpd-item'),
				index = $item.parent('.fpd-grid').children('.fpd-item').index($item);

			if(fpdInstance.mainOptions.saveActionBrowserStorage) {

					var savedProducts = _getSavedProducts();
					savedProducts.splice(index, 1);

				window.localStorage.setItem(fpdInstance.$container.attr('id'), JSON.stringify(savedProducts));

			}

			fpdInstance.$container.trigger('actionLoad:Remove', [index, $item]);

			$item.remove();

		});

		FPDUtil.updateTooltip($gridWrapper);

		return $gridWrapper.children('.fpd-item:last');

	};

	_initialize();

};

FPDActions.availableActions = [
	'print',
	'reset-product',
	'undo',
	'redo',
	'info',
	'save',
	'load',
	'manage-layers',
	'snap',
	'qr-code',
	'zoom',
	'download',
	'magnify-glass',
	'preview-lightbox',
	'ruler',
	'previous-view',
	'next-view',
	'guided-tour'
];

FPDActions.rulerHorImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAAXNSR0IArs4c6QAAAMtJREFUaAXt0bENAkEMBVGgJVranja5lq4myH50wUr+wQRDZCHWWPPee+/fdV33yw+iwAdxhUekgCBJwRgEYTjkCkGSgjEIwnDIFYIkBWMQhOGQKwRJCsYgCMMhVwiSFIxBEIZDrhAkKRiDIAyHXCFIUjAGQRgOuUKQpGAMgjAccoUgScEYjkHWWt+Tk/3dc6XTLscgz3/jt+0CgrSLDvcJMgzYfi5Iu+hwnyDDgO3ngrSLDvcJMgzYfi5Iu+hwnyDDgO3ngrSLDvf9ARH1Efg/D4CQAAAAAElFTkSuQmCC';

FPDActions.rulerVerImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAABkCAYAAACRiYAFAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAAA40lEQVRoBe2UwQ3CUBTD+KzUYXvpSHQmECzgIlk8CbnXRonqvGZt2/a4DTz3gcxPZME/Ix/q/0e99n1/Xv3M4zjOq1rSrZaLEFnv+48tkugTakRkCVouiyT6dNWIyBKE2iKJPmOoWy7sxhKMdVywVSH6hBoRWYKWyyKJPl01IrIEobZIos8Y6pYLu7EEYx0XbFWIPqFGRJag5bJIok9XjYgsQagtkugzhrrlwm4swVjHBVsVok+oEZElaLkskujTVSMiSxBqiyT6jKFuubAbSzDWccFWhegTakRkCb5aLiv07fMCuAVB+Jp9DBgAAAAASUVORK5CYII=';


var FPDImageEditor = function($container, targetElement, fpdInstance) {

	'use strict';

	$ = jQuery;

	var options = fpdInstance.mainOptions.imageEditorSettings;

	var borderColor = '#2ecc71',
		instance = this,
		canvasWidth = 0,
		canvasHeight = 0,
		$canvasContainer = $container.children('.fpd-image-editor-main'),
		$loader = $container.children('.fpd-loader-wrapper'),
		$svgGroupObjects = $container.children('.fpd-svg-group-objects'),
		$svgGroupTools = $container.children('.fpd-svg-group-tools'),
		fabricCanvas,
		customMaskEnabled = false,
		clippingObject = null,
		fabricImage,
		isEdited = 'none',
		ajaxSettings = fpdInstance.mainOptions.customImageAjaxSettings,
		saveOnServer = ajaxSettings.data && ajaxSettings.data.saveOnServer ? 1 : 0,
		uploadsDir = (ajaxSettings.data && ajaxSettings.data.uploadsDir) ? ajaxSettings.data.uploadsDir : '',
		uploadsDirURL = (ajaxSettings.data && ajaxSettings.data.uploadsDirURL) ? ajaxSettings.data.uploadsDirURL : '',
		isGroup = false,
		imageLoaded = false,
		allowedImageExts = ['jpeg', 'jpg', 'png', 'svg'],
		defaultProps = {
			rotatable: true,
			lockRotation: false,
			resizable: true,
			hasRotatingPoint: true,
			hasControls: true,
			lockUniScaling: false,
			centeredScaling: true,
			objectCaching: false,
			padding: 0,
			cornerColor: fpdInstance.mainOptions.cornerColor ? fpdInstance.mainOptions.cornerColor : fpdInstance.mainOptions.selectedColor,
			borderColor: '#333f48',
			borderDashArray: [2,2],
			rotatingPointOffset: 40,
			cornerStyle: 'circle',
			cornerSize: 16,
			transparentCorners: false,
			cornerStrokeColor: '#333f48',
			borderScaleFactor: 1.5,
		},
		fabricMaskOptions = {
			opacity: 0.3,
			borderColor: '#333f48',
			borderDashArray: [3,3],
			cornerStyle: 'circle',
			cornerSize: 16,
			transparentCorners: false,
			cornerStrokeColor: '#333f48',
	        cornerColor: '#fff',
	        borderScaleFactor: 2,
	        hasRotatingPoint: true,
	        centeredScaling: true,
	        objectCaching: false,
	        __editorMode: true,
	        __imageEditor: true
		};

	var _initialize = function() {

		fpdInstance.deselectElement();

		targetElement.originSource = targetElement.originSource ? targetElement.originSource : targetElement.source;

		instance.responsiveScale = 1;

		$container.addClass('fpd-container')

		$canvasContainer.append('<canvas>');

		var canvasOptions = {
			containerClass: 'fpd-image-editor-canvas-wrapper',
			selection: false,
			hoverCursor: 'pointer',
			controlsAboveOverlay: true,
			centeredScaling: true,
			allowTouchScrolling: true,
			preserveObjectStacking: true,
			enableRetinaScaling: false,
			objectCaching: false,
			renderOnAddRemove: true
		};

		fabricCanvas = new fabric.Canvas($canvasContainer.children('canvas:last').get(0), canvasOptions);

		var startCoords = {},
			drawClipping = false;

		fabricCanvas.on({
			'mouse:down': function(opts) {

				if(!clippingObject && customMaskEnabled) {

					drawClipping = true;

					var mouse = fabricCanvas.getPointer(opts.e);

					startCoords.x = mouse.x;
					startCoords.y = mouse.y;

					clippingObject = new fabric.Rect($.extend({}, {
				        width: 0,
				        height: 0,
				        left: mouse.x / instance.responsiveScale,
				        top: mouse.y / instance.responsiveScale,
				        fill: '#000'
				    }, fabricMaskOptions));

				     _resizeCanvas();

				    fabricCanvas.add(clippingObject);
				    fabricCanvas.renderAll();
				    fabricCanvas.setActiveObject(clippingObject);

				}

			},
			'mouse:move': function(opts) {

				if(drawClipping) {

					var mouse = fabricCanvas.getPointer(opts.e),
						w = Math.abs(mouse.x - startCoords.x),
				    	h = Math.abs(mouse.y - startCoords.y);

				    if (!w || !h) {
				        return false;
				    }

				    clippingObject.setOptions({
						width: w / instance.responsiveScale,
						height: h / instance.responsiveScale
					});

					clippingObject.setCoords();
				    fabricCanvas.renderAll();

				}

			},
			'mouse:up': function(opts) {

				drawClipping = false;
			},
			'object:selected': function(opts) {

				if(isGroup) {
					$svgGroupTools.children('.fpd-action-svg-remove-path').removeClass('fpd-disabled');
				}

			},
			'selection:cleared': function(opts) {

				if(isGroup) {
					$svgGroupTools.children('.fpd-action-svg-remove-path').addClass('fpd-disabled');
				}

			},
			'object:modified': function(opts) {
				isEdited = 'yes';
			},
			'object:removed': function(opts) {

				if(imageLoaded) {
					isEdited = 'yes';
				}

			}
		});

		//main menu
		$container.on('click', '.fpd-image-editor-menu > span', function() {

			var $this = $(this),
				id = $this.data('id');

			$this.addClass('fpd-active').siblings().removeClass('fpd-active');
			$container.find('.fpd-tab-content > div').removeClass('fpd-active')
			.filter('[data-id="'+id+'"]').addClass('fpd-active');

		});


		//--- MASK

		if(options.masks && $.isArray(options.masks)) {

			options.masks.forEach(function(svgURL) {

				var title = svgURL.split(/[\\/]/).pop(); //get basename
				title = title.substr(0,title.lastIndexOf('.')); //remove extension

				$container.find('.fpd-mask-selection').append('<span data-mask="'+svgURL+'" class="fpd-tooltip" title="'+title+'" style="background-image: url('+svgURL+')"></span>')
			});

		}

		//mask gets selected
		$container.on('click', '.fpd-mask-selection > span', function() {

			if(!fabricImage) {
				return false;
			}

			var $this = $(this),
				mask = $this.data('mask');

			fabricCanvas.discardActiveObject();
			fabricImage.evented = false;

			fabricCanvas.clipTo = null;
			clippingObject = null;

			if(mask === 'custom-rect') {
				customMaskEnabled = true;
			}
			else {

				fabric.loadSVGFromURL(mask, function(objects, options) {

					//if objects is null, svg is loaded from external server with cors disabled
					var svgGroup = objects ? fabric.util.groupSVGElements(objects, options) : null;

					fabricCanvas.add(svgGroup);

					svgGroup.setOptions($.extend({}, fabricMaskOptions, {opacity: 1, fill: "rgba(0,0,0,0)"}));
					if(fabricCanvas.width > fabricCanvas.height) {
						svgGroup.scaleToHeight((fabricCanvas.height - 80) / instance.responsiveScale);
					}
					else {
						svgGroup.scaleToWidth((fabricCanvas.width - 80) / instance.responsiveScale);
					}

					svgGroup.set('stroke', borderColor).set('strokeWidth', 3 / svgGroup.scaleX);

					clippingObject = svgGroup;
					_resizeCanvas();

					svgGroup.left = 0;
					svgGroup.top = 0;
					svgGroup.setPositionByOrigin(new fabric.Point(canvasWidth * 0.5, canvasHeight * 0.5), 'center', 'center');

					svgGroup.setCoords();
					fabricCanvas.renderAll();

				});

			}

			fabricCanvas.renderAll();
			$container.addClass('fpd-show-secondary');



		});

		//mask: cancel, save
		$container.on('click', '.fpd-mask-cancel, .fpd-mask-save', function() {

			if(!fabricImage) {
				return false;
			}

			fabricImage.evented = true;
			customMaskEnabled = false;

			fabricCanvas.discardActiveObject();

			if(clippingObject) {

				if($(this).hasClass('fpd-mask-save')) {

					_resizeCanvas();

					clippingObject.set('strokeWidth', 0);
					clippingObject.set('fill', 'transparent');
					fabricCanvas.clipTo = function(ctx) {
					  clippingObject.render(ctx);
					};

				}

				fabricCanvas.remove(clippingObject);

			}

			$container.removeClass('fpd-show-secondary');

			isEdited = 'yes';

		});


		//--- FILTERS

		var availableFilters = [
			'none',
			'grayscale',
			'sepia',
			'cold',
			'black_white',
			'old',
			'milk',
			'purple',
			'yellow',
			'monochrome'
		];

		availableFilters.forEach(function(filterName) {

			$container.find('.fpd-content-filters').append('<div data-type="'+filterName+'" style="background-image: url('+FPDFilters[filterName].preview+')" data-defaulttext="'+FPDFilters[filterName].name+'" title="image_editor.filter_'+filterName+'" class="fpd-tooltip"></div>');

		});

		$container.find('.fpd-content-filters [data-defaulttext]').each(function(index, filterElement) {

			fpdInstance.translateElement($(filterElement));

		});

		$container.on('click', '.fpd-content-filters > div', function() {

			if(!fabricImage) {
				return false;
			}

			var removeFilters = [
				'Grayscale',
				'Sepia',
				'Sepia2',
				'ColorMatrix'
			];

			//only one filter is allowed from filters tab
			fabricImage.filters = fabricImage.filters.filter(function(filterItem) {
				return filterItem && removeFilters.indexOf(filterItem.type) === -1;
			});

			fabricImage.filters.push(FPDUtil.getFilter($(this).data('type')));
			_applyFilterRender();

			isEdited = 'yes';

		});


		//--- COLOR MANIPULATION

		$container.on('click', '.fpd-switch-container', function() {

			if(!fabricImage) {
				return false;
			}

			var $this = $(this),
				filterType = $this.data('filter');

			$this.toggleClass('fpd-enabled');
			$this.nextAll('.fpd-range-tooltip:first').toggleClass('fpd-enabled', $this.hasClass('fpd-enabled'));

			if($this.hasClass('fpd-enabled')) {

				//initial values
				var valueObj = {};
				$this.parent().find('.fpd-input-range').each(function(i, input) {
					valueObj[input.name] = parseFloat(input.value);
				});

				_applyFilterValue(filterType, valueObj);

			}
			else {
				_removeFilter(filterType);
			}

		});

		var tooltipTimer = null;
		$container.on('input change', '.fpd-input-range', function() {

			if(!fabricImage) {
				return false;
			}

			var $this = $(this),
				$switchContainer = $this.parent('.fpd-range-tooltip').siblings('.fpd-switch-container:first'),
				filterType = $switchContainer.data('filter'),
				min = parseFloat(this.min),
				max = parseFloat(this.max),
				value = parseFloat(this.value),
				pos = (this.value - min) / (max - min);

			$switchContainer.parent().siblings('.fpd-left').find('.fpd-range-tooltip').removeClass('fpd-moving');

			$this.parent('.fpd-range-tooltip').addClass('fpd-moving')
			.children('.fpd-tooltip').text(value.toFixed(2))
			.css('left', String(pos * 100) + '%');

			var valueObj = {};

			valueObj['color'] = '#fff';
			valueObj[$this.attr('name')] = value;

			_applyFilterValue(filterType, valueObj);

			//hide tooltip after 2 secs
			if(tooltipTimer) {
				clearTimeout(tooltipTimer);
				tooltipTimer = null;
			}

			tooltipTimer = setTimeout(function() {
				$this.parent('.fpd-range-tooltip').removeClass('fpd-moving');
				clearTimeout(tooltipTimer);
				tooltipTimer = null;
			}, 2000);

			isEdited = 'yes';

		});


		//--- SVG Group

		$svgGroupTools.on('click', '.fpd-action-svg-remove-path', function() {

			if(isGroup && fabricCanvas.getActiveObject()) {
				fabricCanvas.remove(fabricCanvas.getActiveObject());
			}

		})

		//--- ACTIONS

		$(document).on('keydown', function(evt) {

			if($container.is(':visible')) {

				if(isGroup && fabricCanvas.getActiveObject()) {

					if(evt.which == 8) {

						fabricCanvas.remove(fabricCanvas.getActiveObject());

					}

				}

			}

		})

		$container.on('click', '.fpd-action-restore', function() {

			if(isGroup) {

				fabric.loadSVGFromURL(targetElement.originSource, function(objects, options) {

					var svgGroup = fabric.util.groupSVGElements(objects, options);

					fabricCanvas.clear();
					instance.loadImage(targetElement.originSource, svgGroup);
					isEdited = 'restored';

				})
			}
			else {

				if(!fabricImage) {
					return false;
				}

				fabricCanvas.clear();
				instance.loadImage(targetElement.originSource);

				isEdited = 'restored';

			}

		});

		$container
		.on('click', '.fpd-action-save', function() {

			fpdInstance.toggleSpinner(true);
			$container.parent().siblings('.fpd-modal-close').click();

			var imageSrc = instance.getImage();

			if(isEdited == 'none') {
				fpdInstance.toggleSpinner(false);
				return false;
			}
			else if(isEdited == 'restored') {
				imageSrc = targetElement.originSource;
			}

			_getSource(imageSrc, function(data) {

				if(data.error) {
					fpdInstance.toggleSpinner(false);
					FPDUtil.showModal(data.error);
					return;
				}

				if($.inArray('svg', targetElement.originSource.split('.')) !== -1) {

					fpdInstance.toggleSpinner(false);

					var elemJSON = fpdInstance.currentViewInstance.getElementJSON(targetElement);
					delete elemJSON['fill'];
					delete elemJSON['svgFill'];
					delete elemJSON['width'];
					delete elemJSON['height'];


					fpdInstance.currentViewInstance.removeElement(targetElement.title, targetElement);
					fpdInstance.currentViewInstance.addElement('image', data.src, targetElement.title, elemJSON);

				}
				else {

					targetElement.source = data.src;
					targetElement.setSrc(data.src, function() {

						fpdInstance.toggleSpinner(false);

						targetElement.setCoords();
						targetElement.canvas.renderAll();

					});

				}

			})

		})

		$container.parents('.fpd-modal-wrapper:first')
		.on('click', '.fpd-modal-close', function() {

		})

		FPDUtil.updateTooltip($container);

	};

	var _getSource = function(imageSrc, callback) {

		var ext = FPDUtil.getFileExtension(imageSrc);

		//check if save on server is enabled and image source is not a url/path
		if(saveOnServer && !allowedImageExts.includes(ext)) {

			var uploadAjaxSettings  = $.extend({}, ajaxSettings);
			uploadAjaxSettings.success = function(data) {

				if(data && data.error === undefined) {

					callback({src: data.image_src});

				}
				else {

					callback({error: data.error});

				}

			};

			uploadAjaxSettings.data = {
				url: imageSrc,
				uploadsDir: uploadsDir,
				uploadsDirURL: uploadsDirURL,
				saveOnServer: saveOnServer
			};

			//ajax post
			$.ajax(uploadAjaxSettings)
			.fail(function(evt) {

				callback({error: evt.statusText});

			});


		}
		else {

			callback({src: imageSrc});

		}

	};

	var _removeFilter = function(type) {

		if(type == 'RemoveWhite') {
			type = 'RemoveColor';
		}

		fabricImage.filters = fabricImage.filters.filter(function(filterItem) {
			return filterItem.type !== type;
		});

		_applyFilterRender();

	};

	var _applyFilterValue = function(type, valueObj) {

		valueObj = valueObj === undefined ? {} : valueObj;

		var existingType = type;
		if(type == 'RemoveWhite') {
			existingType = 'RemoveColor';
		}

		var filterExist = fabricImage.filters.filter(function(filterItem) {
			return filterItem.type === existingType;
		});

		if(filterExist.length > 0) {
			$.extend(filterExist[0], valueObj);
		}
		else {
			var filter = FPDUtil.getFilter(type, valueObj);
			fabricImage.filters.push(filter);
		}

		_applyFilterRender();

	};

	var _resizeCanvas = function () {

		var $canvasWrapper = $container.children('.fpd-image-editor-main');

		instance.responsiveScale = $canvasWrapper.outerWidth() < canvasWidth ? $canvasWrapper.outerWidth() / canvasWidth : 1;
		instance.responsiveScale = parseFloat(Number(instance.responsiveScale.toFixed(7)));
		instance.responsiveScale = instance.responsiveScale > 1 ? 1 : instance.responsiveScale;

		if(clippingObject) {
			clippingObject.left = clippingObject.left * instance.responsiveScale;
			clippingObject.top = clippingObject.top * instance.responsiveScale;
			clippingObject.scaleX = clippingObject.scaleX * instance.responsiveScale;
			clippingObject.scaleY = clippingObject.scaleY * instance.responsiveScale;
			clippingObject.setCoords();
		}

		fabricCanvas
		.setDimensions({
			width: $canvasWrapper.width(),
			height: canvasHeight * instance.responsiveScale
		})
		.setZoom(instance.responsiveScale)
		.calcOffset()
		.renderAll();

	};

	var _applyFilterRender = function() {

		fabricImage.applyFilters();
		fabricCanvas.renderAll();

	};

	this.loadImage = function(imageURL, svgGroup) {

		svgGroup = svgGroup == undefined ? targetElement : svgGroup;

		isGroup = Boolean(fpdInstance.mainOptions.splitMultiSVG) && svgGroup.type == 'group';

		$loader.toggle(true);
		this.reset();

		$container.toggleClass('fpd-is-svg-group', isGroup);

		if(isGroup) {

			isGroup = true;

			svgGroup.clone(function(cloneGroup) {

				cloneGroup.setOptions({
					scaleX: 1,
					scaleY: 1
				})

				var groupObjects = cloneGroup.getObjects();

				canvasWidth = svgGroup.width;
				canvasHeight = svgGroup.height;

				fabricCanvas
				.setDimensions({
					width: canvasWidth,
					height: canvasHeight
				})

				fabricCanvas.add(cloneGroup);
				cloneGroup.center();

				cloneGroup._restoreObjectsState();
		        cloneGroup._objects.forEach(function(item) {

			        fabricCanvas.add(item);
			        item.setOptions(defaultProps);
			        item.setCoords();

		        });

		        fabricCanvas.remove(cloneGroup);

				$loader.toggle(false);
				_resizeCanvas();

				imageLoaded = true;

			})

		}
		else {

			isGroup = false;

			new fabric.Image.fromURL(imageURL, function(fabricImg) {

				fabricImage = fabricImg;
				canvasWidth = fabricImg.width;
				canvasHeight = fabricImg.height;

				FPDUtil.log('AIE - Canvas Width: '+ canvasWidth);
				FPDUtil.log('AIE - Canvas Height: '+ canvasHeight);

				fabricImage.setOptions({
					borderColor: '#333f48',
					borderDashArray: [3,3],
					cornerStyle: 'circle',
					cornerSize: 16,
					transparentCorners: false,
					cornerStrokeColor: '#333f48',
			        cornerColor: '#fff',
			        borderScaleFactor: 2,
			        __editorMode: true,
			        __imageEditor: true
				});

				fabricCanvas.setDimensions({
					width: canvasWidth,
					height: canvasHeight,
				});

				fabricCanvas.add(fabricImg);

				_resizeCanvas();

				$loader.toggle(false);

				imageLoaded = true;

			}, {crossOrigin: "anonymous"});

		}

	};

	this.getImage = function() {

		if(isGroup) {

			return fabricCanvas.toSVG({suppressPreamble: false});

		}

		var maxDimensionSize = parseInt(fabric.textureSize),
			multiplier = 1;

		if(canvasWidth > canvasHeight) {
			multiplier = canvasWidth > maxDimensionSize ? (maxDimensionSize / canvasWidth) : 1;
		}
		else {
			multiplier = canvasHeight > maxDimensionSize ? (maxDimensionSize / canvasHeight) : 1;
		}

		FPDUtil.log('AIE - Data URI Width: '+ (canvasWidth * multiplier));
		FPDUtil.log('AIE - Data URI Height: '+ (canvasHeight * multiplier));

		fabricCanvas.setDimensions({width: canvasWidth, height: canvasHeight}).setZoom(1);

		if(clippingObject) {
			clippingObject.left = (clippingObject.left / instance.responsiveScale) * multiplier;
			clippingObject.top = (clippingObject.top / instance.responsiveScale) * multiplier;
			clippingObject.scaleX = (clippingObject.scaleX / instance.responsiveScale) * multiplier;
			clippingObject.scaleY = (clippingObject.scaleY / instance.responsiveScale) * multiplier;
			clippingObject.setCoords();
		}

		fabricCanvas.renderAll();

		var dataURL = fabricCanvas.toDataURL({format: 'png', multiplier: multiplier});

		FPDUtil.log('AIE - Data URI Size: '+ FPDUtil.getDataUriSize(dataURL));

		_resizeCanvas();

		return dataURL;

	};

	this.reset = function() {

		$container.find('.fpd-switch-container').removeClass('fpd-enabled');

		fabricCanvas.clipTo = null;
		clippingObject = null;
		fabricCanvas.discardActiveObject();

		if(fabricImage) {
			fabricImage.setOptions({
				scaleX: 1,
				scaleY: 1,
				angle: 0,
				left: 0,
				top: 0
			})
			fabricImage.filters = [];

			_applyFilterRender();
		}

	};

	_initialize();

};

var FPDDesignsModule = function(fpdInstance, $module, dynamicDesignId) {

	'use strict';

	$ = jQuery;

	var instance = this,
		searchInLabel = '',
		$head = $module.find('.fpd-head'),
		$scrollArea = $module.find('.fpd-scroll-area'),
		$designsGrid = $module.find('.fpd-grid'),
		lazyClass = fpdInstance.mainOptions.lazyLoad ? 'fpd-hidden' : '',
		currentCategories = null,
		categoriesUsed = false,
		categoryLevelIndexes = [];

	var _initialize = function() {

		if(dynamicDesignId) {
			$module.attr('data-dynamic-designs-id', dynamicDesignId);
		}

		searchInLabel = fpdInstance.getTranslation('modules', 'designs_search_in').toUpperCase();

		$head.find('.fpd-input-search input').keyup(function() {

			if(this.value == '') { //no input, display all
				$designsGrid.children('.fpd-item').css('display', 'block');
			}
			else {
				//hide all items
				$designsGrid.children('.fpd-item').css('display', 'none');

				//only show by input value
			    var searchq = this.value.toLowerCase().trim().split(" ");

			    $designsGrid.children('.fpd-item').filter(function(){

			     	var fullsearchc = 0,
			     		self = this;

				 	$.each( searchq, function( index, value ){
					 	fullsearchc += $(self).is("[data-search*='"+value+"']");
					});

					if(fullsearchc==searchq.length) {return 1;}

			    }).css('display', 'block');
			}

		});

		$head.find('.fpd-back').click(function() {

			if($designsGrid.children('.fpd-category').length > 0) {
				categoryLevelIndexes.pop(); //remove last level index
			}

			//loop through design categories to receive parent category
			var displayCategories = fpdInstance.designs,
				parentCategory;

			categoryLevelIndexes.forEach(function(levelIndex) {

				parentCategory = displayCategories[levelIndex];
				displayCategories = parentCategory.category;

			});

			currentCategories = displayCategories;

			if(displayCategories) { //display first level categories
				_displayCategories(currentCategories, parentCategory);
			}

			//only toggle categories for top level
			if(parentCategory === undefined) {
				instance.toggleCategories();
			}

		});

		//when adding a product after products are set with productsSetup()
		fpdInstance.$container.on('designsSet', function(evt, designs) {

			if(!$.isArray(designs) || designs.length === 0) {
				return;
			}

			if(designs[0].hasOwnProperty('source')) { //check if first object is a design image

				$module.addClass('fpd-single-cat');
				_displayDesigns(designs);

			}
			else {

				if(designs.length > 1 || designs[0].category) { //display categories
					categoriesUsed = true;
					instance.toggleCategories();
				}
				else if(designs.length === 1 && designs[0].designs) { //display designs in category, if only one category exists
					$module.addClass('fpd-single-cat');
					_displayDesigns(designs[0].designs);
				}


			}

		})
		.on('viewSelect', function() {
			instance.toggleCategories();
		})

	};

	var _displayCategories = function(categories, parentCategory) {

		$scrollArea.find('.fpd-grid').empty();
		$head.find('.fpd-input-search input').val('');
		$module.removeClass('fpd-designs-active').addClass('fpd-categories-active');

		categories.forEach(function(category, i) {
			_addDesignCategory(category);
		});

		//set category title
		if(parentCategory) {
			$head.find('.fpd-input-search input').attr('placeholder', searchInLabel + ' ' + parentCategory.title.toUpperCase());
			$module.addClass('fpd-head-visible');
		}

		FPDUtil.refreshLazyLoad($designsGrid, false);
		FPDUtil.createScrollbar($scrollArea);

	};

	var _addDesignCategory = function(category) {

		var thumbnailHTML = category.thumbnail ? '<picture data-img="'+category.thumbnail+'"></picture>' : '',
			itemClass = category.thumbnail ? lazyClass : lazyClass+' fpd-title-centered',
			$lastItem = $('<div/>', {
							'class': 'fpd-category fpd-item '+lazyClass,
							'data-search': category.title.toLowerCase(),
							'html': thumbnailHTML+'<span>'+category.title+'</span>'
						}).appendTo($designsGrid);

		$lastItem.click(function(evt) {

			var $this = $(this),
				index = $this.parent().children('.fpd-item').index($this),
				selectedCategory = currentCategories[index];

			if(selectedCategory.category) {

				categoryLevelIndexes.push(index);
				currentCategories = selectedCategory.category;
				_displayCategories(currentCategories, selectedCategory);

			}
			else {
				_displayDesigns(selectedCategory.designs, selectedCategory.parameters);
			}

			$head.find('.fpd-input-search input').attr('placeholder', searchInLabel + ' ' +$this.children('span').text().toUpperCase());

		});

		if(lazyClass === '' && category.thumbnail) {
			FPDUtil.loadGridImage($lastItem.children('picture'), category.thumbnail);
		}

	};

	var _displayDesigns = function(designObjects, categoryParameters) {

		$scrollArea.find('.fpd-grid').empty();
		$head.find('.fpd-input-search input').val('');
		$module.removeClass('fpd-categories-active').addClass('fpd-designs-active fpd-head-visible');

		var categoryParameters = categoryParameters ? categoryParameters : {};

		designObjects.forEach(function(designObject) {

			designObject.parameters = $.extend({}, categoryParameters, designObject.parameters);
			_addGridDesign(designObject);

		});

		FPDUtil.refreshLazyLoad($designsGrid, false);
		FPDUtil.createScrollbar($scrollArea);
		FPDUtil.updateTooltip();

	};

	//adds a new design to the designs grid
	var _addGridDesign = function(design) {

		design.thumbnail = design.thumbnail === undefined ? design.source : design.thumbnail;

		var $lastItem = $('<div/>', {
							'class': 'fpd-item '+lazyClass,
							'data-title': design.title,
							'data-source': design.source,
							'data-search': design.title.toLowerCase(),
							'data-thumbnail': design.thumbnail,
							'html': '<picture data-img="'+design.thumbnail+'"></picture><span class="fpd-price"></span>'
						}).appendTo($designsGrid);

		$lastItem.click(function() {

			var $this = $(this);

			fpdInstance._addCanvasDesign(
				$this.data('source'),
				$this.data('title'),
				$this.data('parameters')
			);

		}).data('parameters', design.parameters);

		FPDUtil.setItemPrice($lastItem, fpdInstance);

		if(lazyClass === '') {
			FPDUtil.loadGridImage($lastItem.children('picture'), design.thumbnail);
		}

	};

	this.toggleCategories = function() {

		if(!categoriesUsed) {
			return;
		}

		//reset to default view(head hidden, top-level cats are displaying)
		$module.removeClass('fpd-head-visible fpd-single-cat');

		currentCategories = fpdInstance.designs;
		_displayCategories(currentCategories);

		var catTitles = []; //stores category titles that are only visible for UZ or view


		if(fpdInstance.currentViewInstance) {

			var element = fpdInstance.currentViewInstance.currentElement;

			//element (upload zone) has design categories
			if(element && element.uploadZone && element.designCategories) {
				catTitles = fpdInstance.currentViewInstance.currentElement.designCategories;
			}
			//display ror dynamic designs
			else if(dynamicDesignId) {
				catTitles = fpdInstance.mainOptions.dynamicDesigns[dynamicDesignId].categories;
			}
			//all
			else {
				catTitles = fpdInstance.currentViewInstance.options.designCategories;
			}

		}

		//check for particular design categories
		var $allCats = $designsGrid.find('.fpd-category');
		if(catTitles.length > 0) {

			var $visibleCats = $allCats.hide().filter(function() {
				var title = $(this).children('span').text();
				return $.inArray(title, catTitles) > -1;
			}).show($visibleCats);

			if($visibleCats.length === 1) {
				$module.toggleClass('fpd-single-cat');
				$visibleCats.first().click();
				$module.find('.fpd-category').filter(function() { return $(this).css("display") == "block" }).click();
			}

		}
		else {
			$allCats.show();
		}

	};

	_initialize();

};

var FPDProductsModule = function(fpdInstance, $module) {

	'use strict';

	$ = jQuery;

	var instance = this,
		currentCategoryIndex = 0,
		$categoriesDropdown = $module.find('.fpd-product-categories'),
		$scrollArea = $module.children('.fpd-scroll-area'),
		$gridWrapper = $module.find('.fpd-grid'),
		lazyClass = fpdInstance.mainOptions.lazyLoad ? 'fpd-hidden' : '';

	var _initialize = function() {

		_checkProductsLength();

		$categoriesDropdown.children('input').keyup(function() {

			var $categoryItems = $categoriesDropdown.find('.fpd-dropdown-list .fpd-item');
			$categoryItems.hide();

			if(this.value.length === 0) {
				$categoryItems.show();
			}
			else {
				$categoryItems.filter(':containsCaseInsensitive("'+this.value+'")').show();
			}

		});

		$categoriesDropdown.on('click', '.fpd-dropdown-list .fpd-item', function() {

			var $this = $(this);

			currentCategoryIndex = $this.data('value');

			$this.parent().prevAll('.fpd-dropdown-current:first').val($this.text());
			instance.selectCategory(currentCategoryIndex);

			$this.siblings('.fpd-item').show();

			FPDUtil.refreshLazyLoad($gridWrapper, false);

		});

		fpdInstance.$container.on('productsSet', function(evt, products) {

			$categoriesDropdown.find('.fpd-dropdown-list .fpd-item').remove();
			$gridWrapper.empty();

			if(products && products.length > 0) {

				if(products[0].category !== undefined && products.length > 1) { //categories are used

					$module.addClass('fpd-categories-enabled');

					products.forEach(function(categoryItem, i) {
						$categoriesDropdown.find('.fpd-dropdown-list > .fpd-scroll-area')
						.append('<span class="fpd-item" data-value="'+i+'">'+categoryItem.category+'</span>');
					});

				}

				_checkProductsLength();

				instance.selectCategory(0);
			}

			FPDUtil.createScrollbar($categoriesDropdown.find('.fpd-dropdown-list .fpd-scroll-area'));

		});

		//when adding a product after products are set with productsSetup()
		fpdInstance.$container.on('productAdd', function(evt, views, category, catIndex) {

			if(catIndex == currentCategoryIndex) {
				_addGridProduct(views);
			}

		});

	};

	//adds a new product to the products grid
	var _addGridProduct = function(views) {

		var thumbnail = views[0].productThumbnail ? views[0].productThumbnail : views[0].thumbnail,
			productTitle = views[0].productTitle ? views[0].productTitle : views[0].title;

		var $lastItem = $('<div/>', {
							'class': 'fpd-item fpd-tooltip '+lazyClass,
							'data-title': productTitle,
							'data-source': thumbnail,
							'html': '<picture data-img="'+thumbnail+'"></picture>'
						}).appendTo($gridWrapper);

		$lastItem.click(function(evt) {

			evt.preventDefault();

			var $this = $(this),
				index = $gridWrapper.children('.fpd-item').index($this);

			if(fpdInstance.mainOptions.swapProductConfirmation) {

				var $confirm = FPDUtil.showModal(fpdInstance.getTranslation('modules', 'products_confirm_replacement'), false, 'confirm', fpdInstance.$modalContainer);

				$confirm.find('.fpd-confirm').text(fpdInstance.getTranslation('modules', 'products_confirm_button'))
				.click(function() {

					fpdInstance.selectProduct(index, currentCategoryIndex);
					$confirm.find('.fpd-modal-close').click();

				})

			}
			else {
				fpdInstance.selectProduct(index, currentCategoryIndex);
			}


		}).data('views', views);

		if(lazyClass === '') {
			FPDUtil.loadGridImage($lastItem.children('picture'), thumbnail);
		}
		else {
			FPDUtil.refreshLazyLoad($gridWrapper, false);
		}

		FPDUtil.updateTooltip($gridWrapper);

	};

	var _checkProductsLength = function() {

		if(fpdInstance.mainOptions.editorMode) { return; }

		var firstProductItem = fpdInstance.products[0],
			hideProductsModule = firstProductItem === undefined; //hide if no products exists at all

		if(firstProductItem !== undefined) { //at least one product exists

			if((!firstProductItem.hasOwnProperty('category') && fpdInstance.products.length < 2) //no categories are used
				|| (firstProductItem.hasOwnProperty('category') && firstProductItem.products.length < 2 && fpdInstance.products.length < 2)) //categories are used
			{
				hideProductsModule = true;
			}
			else {
				hideProductsModule = false;
			}

		}

		fpdInstance.$container.toggleClass('fpd-products-module-hidden', hideProductsModule);

	};

	this.selectCategory = function(index) {

		$scrollArea.find('.fpd-grid').empty();

		if(fpdInstance.products && fpdInstance.products.length > 0) {

			var productsObj;
			if(fpdInstance.products[0].category !== undefined) { //categories are used
				productsObj = fpdInstance.products[index].products;
				$categoriesDropdown.children('input').val(fpdInstance.products[index].category);
			}
			else {
				productsObj = fpdInstance.products;
			}

			productsObj.forEach(function(productItem) {
				_addGridProduct(productItem);
			});

			FPDUtil.createScrollbar($scrollArea);

		}

	};

	_initialize();

};

var FPDTextModule = function(fpdInstance, $module) {

	'use strict';

	$ = jQuery;

	var currentViewOptions;

	var _initialize = function() {

		$module.find('.fpd-textbox-wrapper').toggle(Boolean(fpdInstance.mainOptions.setTextboxWidth));

		fpdInstance.$container.on('viewSelect', function(evt, index, viewInstance) {

			currentViewOptions = viewInstance.options;

			if(currentViewOptions.customTextParameters && currentViewOptions.customTextParameters.price) {
				var price = fpdInstance.formatPrice(currentViewOptions.customTextParameters.price);
				$module.find('.fpd-btn > .fpd-price').html(' - '+price);
			}
			else {
				$module.find('.fpd-btn > .fpd-price').html('');
			}

		});

		$module.on('click', '.fpd-btn', function() {

			var $input = $(this).prevAll('textarea:first'),
				text = $input.val();

			if(fpdInstance.currentViewInstance && text && text.length > 0) {

				var textboxWidth = parseInt($module.find('.fpd-textbox-width').val());
				$module.find('.fpd-textbox-width').val('');

				var textParams = $.extend({}, currentViewOptions.customTextParameters, {isCustom: true, _addToUZ: fpdInstance.currentViewInstance.currentUploadZone});

				if(!isNaN(textboxWidth)) {
					textParams.textBox = true;
					textParams.width = textboxWidth;
					textParams.resizable = true;
				}

				fpdInstance.addElement(
					'text',
					text,
					text,
					textParams
				);

			}

			$input.val('');

		});

		$module.on('input change', 'textarea', function() {

			var text = this.value,
				maxLength = currentViewOptions ? currentViewOptions.customTextParameters.maxLength : 0,
				maxLines = currentViewOptions ? currentViewOptions.customTextParameters.maxLines : 0;

			//remove emojis
			if(fpdInstance.mainOptions.disableTextEmojis) {
				text = text.replace(FPDEmojisRegex, '');
				text = text.replace(String.fromCharCode(65039), ""); //fix: some emojis left a symbol with char code 65039
			}

			if(maxLength != 0 && text.length > maxLength) {
				text = text.substr(0, maxLength);
			}

			if(maxLines != 0 && text.split("\n").length > maxLines) {
				text = text.replace(/([\s\S]*)\n/, "$1");
			}

			this.value = text;

		});

		if($.isArray(fpdInstance.mainOptions.textTemplates)) {

			var $textTemplatesGrid = $module.find('.fpd-text-templates .fpd-grid');

			fpdInstance.mainOptions.textTemplates.forEach(function(item) {

				var props = item.properties,
					styleAttr = 'font-family:'+ (props.fontFamily ? props.fontFamily : 'Arial');

				styleAttr += '; text-align:'+ (props.textAlign ? props.textAlign : 'left');

				var $lastItem = $('<div/>', {
					'class': 'fpd-item',
					'data-props': JSON.stringify(item.properties),
					'data-text': item.text,
					'html': '<div style="'+styleAttr+'">'+item.text.replace(/(?:\r\n|\r|\n)/g, '<br>')+'</div>'
				}).appendTo($textTemplatesGrid);

				$lastItem.click(function() {

					if(fpdInstance.currentViewInstance) {

						var textParams = $.extend({},
							currentViewOptions.customTextParameters,
							{isCustom: true, _addToUZ: fpdInstance.currentViewInstance.currentUploadZone},
							$(this).data('props')
						);

						fpdInstance.addElement(
							'text',
							this.dataset.text,
							this.dataset.text,
							textParams
						);

					}

				})

			})

			FPDUtil.createScrollbar($textTemplatesGrid.parent('.fpd-scroll-area'));

		}

	};

	_initialize();

};

var FPDLayersModule = {

	createList : function(fpdInstance, $container) {

		var $currentColorList,
			colorDragging = false;

		$container.off();

		//append a list item to the layers list
		var _appendLayerItem = function(element) {

			var colorHtml = '<span></span>';
			if(FPDUtil.elementHasColorSelection(element)) {

				var availableColors = FPDUtil.elementAvailableColors(element, fpdInstance);

				if(element.uploadZone) {
					colorHtml = '<span></span>';
				}
				else if(element.type == FPDPathGroupName && element.getObjects().length > 1) {
					colorHtml = '<span class="fpd-current-color" style="background: '+FPDUtil.getBgCssFromElement(element)+'"></span>';
				}
				else if(availableColors != 1 && (availableColors.length > 1 || (element.type == FPDPathGroupName && element.getObjects().length === 1))) {
					colorHtml = '<span class="fpd-current-color" style="background: '+FPDUtil.getBgCssFromElement(element)+'" data-colors=""></span>';
				}
				else {
					colorHtml = '<input class="fpd-current-color" type="text" value="'+FPDUtil.getBgCssFromElement(element)+'" />'
				}

			}

			var sourceContent = element.title;
			if(FPDUtil.getType(element.type) === 'text' && element.editable) {

				sourceContent = '<textarea>'+element.text+'</textarea>';

			}

			$container.find('.fpd-list').append('<div class="fpd-list-row" id="'+element.id+'"><div class="fpd-cell-0">'+colorHtml+'</div><div class="fpd-cell-1">'+sourceContent+'</div><div class="fpd-cell-2"></div></div>');

			var $lastItem = $container.find('.fpd-list-row:last')
				.data('element', element)
				.data('colors', availableColors);

			if(element.uploadZone) {
				$lastItem.addClass('fpd-add-layer')
				.find('.fpd-cell-2').append('<span class="fpd-icon-add"></span>');

				if(element.uploadZoneRemovable) {
					$lastItem.find('.fpd-icon-add').after('<span class="fpd-remove-element"><span class="fpd-icon-remove"></span></span>');
				}

			}
			else {

				var lockIcon = element.locked ? 'fpd-icon-locked-full' : 'fpd-icon-unlocked',
					reorderHtml = element.zChangeable ? '<span class="fpd-icon-reorder"></span>' : '';

				$lastItem.find('.fpd-cell-2').append(reorderHtml+'<span class="fpd-lock-element"><span class="'+lockIcon+'"></span></span>');

				if(element.removable || element.__editorMode) {
					$lastItem.find('.fpd-lock-element').after('<span class="fpd-remove-element"><span class="fpd-icon-remove"></span></span>');
				}

				$lastItem.toggleClass('fpd-locked', element.locked);

			}

		};

		//destroy all color pickers and empty list
		$container.find('.fpd-current-color').spectrum('destroy');
		$container.find('.fpd-list').empty();

		fpdInstance.getElements(fpdInstance.currentViewIndex).forEach(function(element) {

			if(FPDUtil.elementIsEditable(element)) {
				_appendLayerItem(element);
			}

		});

		FPDUtil.createScrollbar($container.find('.fpd-scroll-area'));

		//sortable layers list
		var sortDir = 0;
		$container.find('.fpd-list').sortable({
			handle: '.fpd-icon-reorder',
			placeholder: 'fpd-list-row fpd-sortable-placeholder',
			scroll: false,
			axis: 'y',
			containment: 'parent',
			items: '.fpd-list-row:not(.fpd-locked)',
			start: function(evt, ui) {
				sortDir = ui.originalPosition.top;
			},
			change: function(evt, ui) {

				var targetElement = fpdInstance.getElementByID(ui.item.attr('id')),
					relatedItem;

				if(ui.position.top > sortDir) { //down
					relatedItem = ui.placeholder.prevAll(".fpd-list-row:not(.ui-sortable-helper)").first();
				}
				else { //up
					relatedItem = ui.placeholder.nextAll(".fpd-list-row:not(.ui-sortable-helper)").first();
				}

				var fabricElem = fpdInstance.currentViewInstance.getElementByID(relatedItem.attr('id')),
					index = fpdInstance.currentViewInstance.getZIndex(fabricElem);

				fpdInstance.setElementParameters({z: index}, targetElement);

				sortDir = ui.position.top;

			}
		});

		$container.find('input.fpd-current-color').spectrum('destroy').spectrum({
			flat: false,
			preferredFormat: "hex",
			showInput: true,
			showInitial: true,
			showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
			palette: fpdInstance.mainOptions.colorPickerPalette,
			showButtons: false,
			show: function(color) {

				FPDUtil.spectrumColorNames($(this).spectrum('container'), fpdInstance);

				var element = $(this).parents('.fpd-list-row:first').data('element');
				element._tempFill = color.toHexString();

			},
			move: function(color) {

				var element = $(this).parents('.fpd-list-row:first').data('element');
				//only non-png images are chaning while dragging
				if(colorDragging === false || FPDUtil.elementIsColorizable(element) !== 'png') {
					fpdInstance.currentViewInstance.changeColor(element, color.toHexString());
				}

			},
			change: function(color) {

				$(document).unbind("click.spectrum"); //fix, otherwise change is fired on every click
				var element = $(this).parents('.fpd-list-row:first').data('element');
				fpdInstance.currentViewInstance.setElementParameters({fill: color.toHexString()}, element);

			}
		})
		.on('beforeShow.spectrum', function(e, tinycolor) {
			if($currentColorList) {
				$currentColorList.remove();
				$currentColorList = null;
			}
		})
		.on('dragstart.spectrum', function() {
			colorDragging = true;
		})
		.on('dragstop.spectrum', function(evt, color) {
			colorDragging = false;
			var element = $(this).parents('.fpd-list-row:first').data('element');
			fpdInstance.currentViewInstance.changeColor(element, color.toHexString());
		});

		$container.off('click', '.fpd-current-color') //unregister click, otherwise triggers multi-times when changing view
		.on('click', '.fpd-current-color', function(evt) { //open sub

			evt.stopPropagation();

			$container.find('.fpd-path-colorpicker').spectrum('destroy');
			$container.find('input.fpd-current-color').spectrum('hide');

			var $listItem = $(this).parents('.fpd-list-row'),
				element = $listItem.data('element'),
				availableColors = $listItem.data('colors');

			//clicked on opened sub colors, just close it
			if($currentColorList && $listItem.children('.fpd-scroll-area').length > 0) {
				$currentColorList.slideUp(200, function(){ $(this).remove(); });
				$currentColorList = null;
				return;
			}

			//close another sub colors
			if($currentColorList) {
				$currentColorList.slideUp(200, function(){ $(this).remove(); });
				$currentColorList = null;
			}

			if(availableColors.length > 0) {

				$listItem.append('<div class="fpd-scroll-area"><div class="fpd-color-palette fpd-grid"></div></div>');

				for(var i=0; i < availableColors.length; ++i) {

					var item;
					if(element.type === FPDPathGroupName && element.getObjects().length > 1) {

						item = '<input class="fpd-path-colorpicker" type="text" value="'+availableColors[i]+'" />';

					}
					else {

						var tooltipTitle = fpdInstance.mainOptions.hexNames[availableColors[i].replace('#', '').toLowerCase()];
						tooltipTitle = tooltipTitle ? tooltipTitle : availableColors[i];

						item = '<div class="fpd-item fpd-tooltip" title="'+tooltipTitle+'" style="background-color: '+availableColors[i]+'" data-color="'+availableColors[i]+'"></div>';

					}

					$listItem.find('.fpd-color-palette').append(item);
				}

				FPDUtil.createScrollbar($listItem.children('.fpd-scroll-area'));
				FPDUtil.updateTooltip($listItem);

				if(element.type === FPDPathGroupName && element.getObjects().length > 1) {

					$listItem.find('.fpd-path-colorpicker').spectrum({
						showPaletteOnly: $.isArray(element.colors),
						preferredFormat: "hex",
						showInput: true,
						showInitial: true,
						showButtons: false,
						showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
						palette: $.isArray(element.colors) ? element.colors : fpdInstance.mainOptions.colorPickerPalette,
						show: function(color) {

							var $listItem = $(this).parents('.fpd-list-row'),
								element = $listItem.data('element');

							var svgColors = FPDUtil.changePathColor(
								element,
								$listItem.find('.fpd-path-colorpicker').index(this),
								color
							);

							FPDUtil.spectrumColorNames($(this).spectrum('container'), fpdInstance);

							element._tempFill = svgColors;

						},
						move: function(color) {

							var $listItem = $(this).parents('.fpd-list-row'),
								element = $listItem.data('element');

							var svgColors = FPDUtil.changePathColor(
								element,
								$listItem.find('.fpd-path-colorpicker').index(this),
								color
							);

							fpdInstance.currentViewInstance.changeColor(element, svgColors);

						},
						change: function(color) {

							var $listItem = $(this).parents('.fpd-list-row'),
								element = $listItem.data('element');

							var svgColors = FPDUtil.changePathColor(
								element,
								$listItem.find('.fpd-path-colorpicker').index(this),
								color
							);

							$(document).unbind("click.spectrum"); //fix, otherwise change is fired on every click
							fpdInstance.currentViewInstance.setElementParameters({fill: svgColors}, element);

						}
					});

				}
				else {

					if($.isArray(element.patterns) && (FPDUtil.isSVG(element) || FPDUtil.getType(element.type) === 'text')) {

						element.patterns.forEach(function(pattern) {

							var patternTitle = pattern.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, "").replace('_', ' '),
								patternLabel = '<div class="fpd-label">'+patternTitle+'</div>';

							$listItem.find('.fpd-color-palette').append('<div data-pattern="'+pattern+'" style="background-image: url('+pattern+')" class="fpd-item fpd-tooltip fpd-pattern" title="'+patternTitle+'"></div>');


						})

						FPDUtil.updateTooltip($listItem);

					}

				}

				$currentColorList = $listItem.children('.fpd-scroll-area').slideDown(300);

			}

		});

		//select color from color palette
		$container.on('click', '.fpd-color-palette .fpd-item', function(evt) {

			evt.stopPropagation();

			var $this = $(this),
				$listItem = $this.parents('.fpd-list-row'),
				element = $listItem.data('element'),
				fill = $this.data('pattern') ? $this.data('pattern') : $this.data('color'),
				paramsObj = $this.data('pattern') ? {pattern: fill} : {fill: fill};

			$listItem.find('.fpd-current-color').css('background', $this.data('pattern') ? 'url('+fill+')' : fill);

			//if svg has one path
			if(element.type == FPDPathGroupName) {

				newColor = FPDUtil.changePathColor(
					element,
					0,
					fill
				);

			}

			fpdInstance.currentViewInstance.setElementParameters(paramsObj, element);

		});

		//select associated element on stage when choosing one from the layers list
		$container.on('click', '.fpd-list-row', function(evt) {

			var $this = $(this);

			if($this.hasClass('fpd-locked') ||  $(evt.target).is('textarea')) {
				return;
			}

			var targetElement = fpdInstance.getElementByID(this.id);
			if(targetElement) {
				targetElement.canvas.setActiveObject(targetElement).renderAll();
			}

		});

		//lock element
		$container.on('click', '.fpd-lock-element',function(evt) {

			evt.stopPropagation();

			var $this = $(this),
				element = $this.parents('.fpd-list-row').data('element');

			if($currentColorList) {
				$currentColorList.slideUp(200, function(){ $(this).remove(); });
				$currentColorList = null;
			}

			element.evented = !element.evented;
			element.locked = !element.evented;

			$this.children('span').toggleClass('fpd-icon-unlocked', element.evented)
			.toggleClass('fpd-icon-locked-full', !element.evented);

			$this.parents('.fpd-list-row').toggleClass('fpd-locked', !element.evented);
			$this.parents('.fpd-list:first').sortable( 'refresh' );

		});

		//remove element
		$container.on('click', '.fpd-remove-element',function(evt) {

			evt.stopPropagation();

			var $listItem = $(this).parents('.fpd-list-row');

			fpdInstance.currentViewInstance.removeElement($listItem.data('element'));

		});

		//text is changed via textarea
		$container.on('keyup', 'textarea',function(evt) {

			evt.stopPropagation();

			var $this = $(this),
				element = $this.parents('.fpd-list-row').data('element');

			//remove emojis
			if(fpdInstance.mainOptions.disableTextEmojis) {
				this.value = this.value.replace(FPDEmojisRegex, '');
				this.value = this.value.replace(String.fromCharCode(65039), ""); //fix: some emojis left a symbol with char code 65039
			}

			fpdInstance.currentViewInstance.setElementParameters({text: this.value}, element);

		});

		//text is changed in canvas
		var _textChanged = function(evt, element, parameters) {

			if(parameters.text) {

				if(document.activeElement && $(document.activeElement).parent('.fpd-cell-1').length) {
					return;
				}

				$container.find('.fpd-list')
				.find('[id="'+element.id+'"] textarea').val(parameters.text);
			}

		};

		fpdInstance.$container.off('elementModify', _textChanged);
		fpdInstance.$container.on('elementModify', _textChanged);

		//element color change
		var _elementColorChanged = function(evt, element, hex, colorLinking) {

			var $currentColor = $container.find('.fpd-list')
			.find('[id="'+element.id+'"] .fpd-current-color');

			if($currentColor.is('input')) {
				$currentColor.spectrum('set', hex);
			}
			else {
				$currentColor.css('background', hex);
			}

		};

		fpdInstance.$container.off('elementColorChange', _elementColorChanged);
		fpdInstance.$container.on('elementColorChange', _elementColorChanged);

	}

};

var FPDImagesModule = function(fpdInstance, $module) {

	'use strict';

	$ = jQuery;

	var lazyClass = fpdInstance.mainOptions.lazyLoad ? 'fpd-hidden' : '',
		$imageInput = $module.find('.fpd-input-image'),
		$uploadScrollArea = $module.find('[data-context="upload"] .fpd-scroll-area'),
		$uploadGrid = $uploadScrollArea.find('.fpd-grid'),
		uploadCounter = 0,
		firstUploadDone = false,
		allUploadZones = [],
		totalUploadFiles = 0,
		$fbAlbumDropdown = $module.find('.fpd-facebook-albums'),
		$fbScrollArea = $module.find('[data-context="facebook"] .fpd-scroll-area'),
		$fbGrid = $fbScrollArea.find('.fpd-grid'),
		$instaScrollArea = $module.find('[data-context="instagram"] .fpd-scroll-area'),
		$instaGrid = $instaScrollArea.find('.fpd-grid'),
		facebookAppId = fpdInstance.mainOptions.facebookAppId,
		instagramClientId = fpdInstance.mainOptions.instagramClientId,
		instagramRedirectUri = fpdInstance.mainOptions.instagramRedirectUri,
		instaAccessToken = null,
		instaLoadingStack = false,
		instaNextStack = null,
		localStorageAvailable = FPDUtil.localStorageAvailable(),
		ajaxSettings = fpdInstance.mainOptions.customImageAjaxSettings,
		saveOnServer = ajaxSettings.data && ajaxSettings.data.saveOnServer ? 1 : 0,
		uploadsDir = (ajaxSettings.data && ajaxSettings.data.uploadsDir) ? ajaxSettings.data.uploadsDir : '',
		uploadsDirURL = (ajaxSettings.data && ajaxSettings.data.uploadsDirURL) ? ajaxSettings.data.uploadsDirURL : '',
		allowedFileTypes = fpdInstance.mainOptions.allowedImageTypes,
		pixabayApiKey = fpdInstance.mainOptions.pixabayApiKey,
		$pixabayScrollArea = $module.find('[data-context="pixabay"] .fpd-scroll-area'),
		$pixabayGrid = $pixabayScrollArea.find('.fpd-grid'),
		pixabayLoadingStack = false,
		pixabayCurrentQuery = '',
		pixabayPage = 1,
		dpApiKey = fpdInstance.mainOptions.depositphotosApiKey,
		$dpScrollArea = $module.find('[data-context="depositphotos"] .fpd-scroll-area'),
		$dpGrid = $dpScrollArea.find('.fpd-grid'),
		dpLoadingStack = false,
		dpCurrentType = '',
		dpCurrentQuery = '',
		dpCurrentCat = null,
		dpOffset = 0,
		$body = $('body'),
		$window = $(window);

	var _initialize = function() {

		//set price in upload drop zone
		fpdInstance.$container
		.on('viewSelect secondaryModuleCalled', function(evt) {

			if(!fpdInstance.currentViewInstance) { return; }

			var currentViewOptions = fpdInstance.currentViewInstance.options,
				price = null;

			if(fpdInstance.currentViewInstance.currentUploadZone) { //get upload zone price

				var uploadZone = fpdInstance.currentViewInstance.getUploadZone(fpdInstance.currentViewInstance.currentUploadZone);
				if(uploadZone && uploadZone.price) {
					price = uploadZone.price;
				}

			}

			if(price == null && currentViewOptions.customImageParameters && currentViewOptions.customImageParameters.price) {
				price = fpdInstance.formatPrice(currentViewOptions.customImageParameters.price);

			}

			$module.find('.fpd-upload-zone .fpd-price').html(price ? price : '');

		})
		.on('productCreate', function() {
			firstUploadDone = false;
		})


		//ALL

		//click on image item
		$module.on('click', '.fpd-grid .fpd-item:not(.fpd-category):not(.fpd-loading)', function(evt) {

			if(!fpdInstance._loadingCustomImage) {
				fpdInstance._addGridItemToStage($(this));
			}

		});


		//IMAGE UPLOAD
		if(allowedFileTypes.indexOf('jpeg') !== -1 && allowedFileTypes.indexOf('jpg') === -1) {
			allowedFileTypes.push('jpg');
		}

		var acceptTypes = [];
		allowedFileTypes.forEach(function(imageTpye) {

			if(imageTpye == 'pdf') {
				acceptTypes.push('application/pdf')
			}
			else {

				if(imageTpye == 'svg') {
					imageTpye += '+xml';
				}
				acceptTypes.push('image/'+imageTpye);

			}

		});
		$imageInput.attr('accept', acceptTypes.join());

		var $uploadZone = $module.find('.fpd-upload-zone');

		$uploadZone.click(function(evt) {

			evt.preventDefault();
			$imageInput.click();

		})
		.on('dragover dragleave', function(evt) {

			evt.stopPropagation();
			evt.preventDefault();

			$(this).toggleClass('fpd-hover', evt.type === 'dragover');

		});

		var _parseFiles = function(evt) {

			evt.stopPropagation();
			evt.preventDefault();

			var files = evt.target.files || evt.dataTransfer.files;

			if(fpdInstance.mainOptions.uploadAgreementModal) {

				var $confirm = FPDUtil.showModal(fpdInstance.getTranslation('modules', 'images_agreement'), false, 'confirm', fpdInstance.$modalContainer);
				$confirm.find('.fpd-confirm').text(fpdInstance.getTranslation('modules', 'images_confirm_button')).click(function() {

					$confirm.find('.fpd-modal-close').click();

					//timeout to wait for modal closing
					setTimeout(function() {
						_addFiles(files);
					}, 300);

				});

			}
			else {
				_addFiles(files);
			}


		};

		var _addFiles = function(files) {

			uploadCounter = 0;
			totalUploadFiles = files.length;

			for(var i=0; i < fpdInstance.currentViews.length; ++i) {

				fpdInstance.getElements(i).forEach(function(elem) {

					if(elem.uploadZone) {
						allUploadZones.push({uz: elem.title, viewIndex: i});
					}

				});

			}

			if(window.FileReader) {

				var addFirstToStage = true;
				fpdInstance._loadingCustomImage = true;

				for(var i=0; i < files.length; ++i) {

					var extension = files[i].name.split('.').pop().toLowerCase();

					if(allowedFileTypes.indexOf(extension) > -1) {
						_addUploadedImage(files[i], addFirstToStage);
						addFirstToStage = false;
					}

				}

			}

			$uploadZone.removeClass('fpd-hover');
			$imageInput.val('');

		};

		if($uploadZone.get(0)) {
			$uploadZone.get(0).addEventListener('drop', _parseFiles, false);
		}

		$module.find('.fpd-upload-form').on('change', _parseFiles);

		//window.localStorage.removeItem('fpd_uploaded_images');
		if(localStorageAvailable && window.localStorage.getItem('fpd_uploaded_images')) {

			var storageImages = JSON.parse(window.localStorage.getItem('fpd_uploaded_images'));

			storageImages.forEach(function(storageImage) {

				_addThumbnail(storageImage.url, storageImage.title)

				var image = new Image();
				image.src = storageImage.url;
				image.onerror = function() {

					var removeIndex = null;
					storageImages.forEach(function(storedImg, key) {
						if(storedImg.url == image.src) { removeIndex = key; }
					})

					if(removeIndex != null) {
						storageImages.splice(removeIndex, 1);
						window.localStorage.setItem('fpd_uploaded_images', JSON.stringify(storageImages));
					}

				}

			});

		}

		$module.find('.fpd-module-tabs-content [data-context="upload"]')
		.on('click', '.fpd-icon-remove', function(evt) {

			evt.stopPropagation();

			var $thumbnail = $(this).parents('.fpd-item:first'),
				index = $uploadGrid.children('.fpd-item').index($thumbnail);

			if(!$thumbnail.hasClass('fpd-loading')) {

				var storageImages = JSON.parse(window.localStorage.getItem('fpd_uploaded_images'));
				storageImages.splice(index, 1);
				window.localStorage.setItem('fpd_uploaded_images', JSON.stringify(storageImages));

			}

			if($thumbnail.data('xhr')) {
				$thumbnail.data('xhr').abort();
			}

			$thumbnail.remove();
			$('.fpd-thumbnail-preview').remove();

		});


		//FACEBOOK
		if(facebookAppId && facebookAppId.length > 5) {

			$module.find('.fpd-module-tabs [data-context="facebook"]').removeClass('fpd-hidden');
			_initFacebook();

		}

		//INSTAGRAM
		if(instagramClientId && instagramClientId.length > 5) {

			$module.find('.fpd-module-tabs [data-context="instagram"]')
			.removeClass('fpd-hidden')
			.on('click', function() {

				if($instaGrid.children('.fpd-item').length > 0) {
					return;
				}

				_authInstagram();

			});

			$instaScrollArea.on('_sbOnTotalScroll', function() {

				if(instaNextStack !== null && !instaLoadingStack) {
					_loadInstaImages(instaNextStack, false);
				}

			});

		}


		//PIXABAY
		if(pixabayApiKey && pixabayApiKey.length > 5) {

			$module.find('.fpd-module-tabs [data-context="pixabay"]').removeClass('fpd-hidden')
			.on('click', function() {

				if($pixabayGrid.children('.fpd-item').length > 0) {
					return;
				}

				_loadPixabayImages();

			});

			$module.on('keypress', '.fpd-module-tabs-content [data-context="pixabay"] input[type="text"]', function(evt) {

				if(evt.which == 13) {

					pixabayPage = 1;
					_loadPixabayImages(this.value);

				}

			});

			$pixabayScrollArea.on('_sbOnTotalScroll', function() {

				if(!pixabayLoadingStack) {

					pixabayPage++;
					_loadPixabayImages(undefined, false);

				}

			});

		}


		//DEPSOITPHOTOS
		if(dpApiKey && dpApiKey.length > 5) {

			$module.find('.fpd-module-tabs [data-context="depositphotos"]').removeClass('fpd-hidden')
			.on('click', function() {

				if($dpGrid.children('.fpd-item').length > 0) {
					return;
				}

				_loadDPImages('list-cats', null);

			});

			$module
			.on('keypress', '.fpd-module-tabs-content [data-context="depositphotos"] input[type="text"]', function(evt) {

				if(evt.which == 13) {

					dpOffset = 0;
					_loadDPImages('search', this.value);

				}

			})
			.on('click', '.fpd-module-tabs-content [data-context="depositphotos"] .fpd-category', function() {

				var $this = $(this),
					catTitle = $this.text();

				$module.find('.fpd-module-tabs-content [data-context="depositphotos"] .fpd-input-search input').attr('placeholder', fpdInstance.getTranslation('modules', 'depositphotos_search_category') + catTitle);

				dpCurrentCat = $this.data('category');
				_loadDPImages('search-cats', dpCurrentCat);

			})
			.on('click', '.fpd-module-tabs-content [data-context="depositphotos"] .fpd-back', function() {

				dpOffset = 0;
				dpCurrentCat = null;
				$module.find('.fpd-module-tabs-content [data-context="depositphotos"] .fpd-input-search input')
				.val('').attr('placeholder', fpdInstance.getTranslation('modules', 'depositphotos_search'));

				_loadDPImages('list-cats', null);

			});

			$dpScrollArea.on('_sbOnTotalScroll', function() {

				if(!dpLoadingStack && $dpScrollArea.prev('.fpd-cats-shown').length == 0) {

					dpOffset++;
					_loadDPImages(dpCurrentType, dpCurrentQuery, false);

				}

			});

		}

		//hide tabs when only one is active
		if($module.find('.fpd-module-tabs > :not(.fpd-hidden)').length < 2 ) {
			$module.addClass('fpd-hide-tabs');
		}

		$module.children('.fpd-module-tabs').children('div:not(.fpd-hidden):first').click();

	};

	var _addUploadedImage = function(file, addToStage) {

		//check maximum allowed size
		var maxSizeBytes = fpdInstance.mainOptions.customImageParameters.maxSize * 1024 * 1024;

		if(file.size > maxSizeBytes) {
			FPDUtil.showMessage(fpdInstance.getTranslation('misc', 'maximum_size_info').replace('%filename', file.name).replace('%mb', fpdInstance.mainOptions.customImageParameters.maxSize));
			fpdInstance._loadingCustomImage = false;
			return;
		}

		//load image with FileReader
		var reader = new FileReader();

    	reader.onload = function (evt) {

	    	//-- PDF UPLOAD

	    	if(file.type === 'application/pdf') {

		    	var $uploadSnackBar = FPDUtil.showMessage(fpdInstance.getTranslation('modules', 'images_pdf_upload_info'), false);

				var formdata = new FormData();
				formdata.append('uploadsDir', uploadsDir);
				formdata.append('uploadsDirURL', uploadsDirURL);
				formdata.append('pdf', file);

				var uploadAjaxSettings  = $.extend({}, ajaxSettings);
				uploadAjaxSettings.data = formdata;
				uploadAjaxSettings.processData = false;
				uploadAjaxSettings.contentType = false;

		        uploadAjaxSettings.success = function(data) {

					if(data && data.error === undefined) {

						totalUploadFiles--;
						data.pdf_images.forEach(function(pdfImageData, i) {

							var $lastItem = _addThumbnail(pdfImageData.image_url, pdfImageData.filename);

							addToStage = i == 0;

							_addToStage($lastItem, addToStage);
							_storeUploadedImage(pdfImageData.image_url, pdfImageData.filename);
							uploadCounter++;

						})

						$uploadSnackBar.remove();

						totalUploadFiles++;

					}
					else {
						$uploadSnackBar.remove();
						FPDUtil.showModal(data.error);
					}

				};

				var xhr = $.ajax(uploadAjaxSettings)
				.fail(function(evt) {

					if(evt.statusText !== 'abort') {

						fpdInstance._loadingCustomImage = false;
						$uploadSnackBar.remove();
						FPDUtil.showModal(evt.statusText);

					}

				});

		    	return;
	    	}

	    	//-- IMAGE UPLOAD

			//check image resolution of jpeg
	    	if(file.type === 'image/jpeg' && fpdInstance.mainOptions.customImageParameters.minDPI) {

		    	var jpeg = new JpegMeta.JpegFile(atob(this.result.replace(/^.*?,/,'')), file.name),
		    		realRes = null;

		    	if(jpeg.tiff && jpeg.tiff.XResolution && jpeg.tiff.XResolution.value) {

			    	var xResDen = jpeg.tiff.XResolution.value.den,
			    		xResNum = jpeg.tiff.XResolution.value.num;

			    	realRes = xResNum / xResDen;

			    }
			    else if(jpeg.jfif && jpeg.jfif.Xdensity && jpeg.jfif.Xdensity.value) {
				    realRes = jpeg.jfif.Xdensity.value;
			    }

		    	if(realRes !== null) {

					FPDUtil.log(file.name+' Real Resolution: '+ realRes, 'info');

					if(realRes < fpdInstance.mainOptions.customImageParameters.minDPI) {
						FPDUtil.showModal(fpdInstance.getTranslation('misc', 'minimum_dpi_info').replace('%dpi', fpdInstance.mainOptions.customImageParameters.minDPI), false, '', fpdInstance.$modalContainer);
						fpdInstance._loadingCustomImage = false;
						return false;
					}

		    	}
		    	else {
			    	FPDUtil.log(file.name + ': Resolution is not accessible.', 'info');
		    	}

	    	}

	    	var image = this.result,
				$lastItem = _addThumbnail(image, file.name);

			if(saveOnServer) {

				$lastItem
				.addClass('fpd-loading')
				.append('<div class="fpd-loading-bar"><div class="fpd-loading-progress"></div></div>');

			}

			//check image dimensions
			var checkDimImage = new Image();
			checkDimImage.onload = function() {

				var imageH = this.height,
					imageW = this.width,
					currentCustomImageParameters = fpdInstance.currentViewInstance.options.customImageParameters;

				if(FPDUtil.checkImageDimensions(fpdInstance, imageW, imageH)) {

					if(saveOnServer) {

						var formdata = new FormData();
						formdata.append('uploadsDir', uploadsDir);
						formdata.append('uploadsDirURL', uploadsDirURL);
						formdata.append('images[]', file);

						var uploadAjaxSettings  = $.extend({}, ajaxSettings);
						uploadAjaxSettings.data = formdata;
						uploadAjaxSettings.processData = false;
						uploadAjaxSettings.contentType = false;

						//upload progress
						uploadAjaxSettings.xhr = function() {

				            var xhr = $.ajaxSettings.xhr();

				            if(xhr.upload) {

				                xhr.upload.addEventListener('progress', function(evt) {

									if(evt.lengthComputable) {

								        var max = evt.total,
								        	current = evt.loaded,
								        	percentage = parseInt((current * 100)/max);

										$lastItem.find('.fpd-loading-progress').css('width', percentage+'%');

								    }

								}, false);
				            }

				            return xhr;

				        };

				        uploadAjaxSettings.success = function(data) {

							//loading thumbnail was removed
					        if($lastItem.parents('body').length == 0) {
						        return;
					        }

							if(data && data.error === undefined) {

								_storeUploadedImage(data.image_src, data.filename);

								$lastItem
								.data('source', data.image_src) //update source to local server image
								.removeClass('fpd-loading')
								.children('.fpd-loading-bar').remove();

								_addToStage($lastItem, addToStage);
								uploadCounter++;

							}
							else { //upload error, e.g. max_upload_size

								fpdInstance._loadingCustomImage = false;
								$lastItem.remove();
								FPDUtil.showModal(data.error);

							}

						};

				        var xhr = $.ajax(uploadAjaxSettings)
				        .fail(function(evt) {

							fpdInstance._loadingCustomImage = false;

							if(evt.statusText !== 'abort') {

								$lastItem.remove();
								FPDUtil.showModal(evt.statusText);

							}

						});

						$lastItem.data('xhr', xhr);

					}
					else { //do not save on server

						_storeUploadedImage(image, file.name);
						_addToStage($lastItem, addToStage);

						uploadCounter++;

					}

				}
				else { //remove thumbnail when dimensions are not in the range
					$lastItem.remove();
					fpdInstance.currentViewInstance.currentUploadZone = null;
				}

			};

			checkDimImage.src = image;

		}

		//add file to start loading
		reader.readAsDataURL(file);

	};

	var _addToStage = function($item, addToStage) {

		if(!firstUploadDone && fpdInstance.mainOptions.autoFillUploadZones) {

			var targetUploadzone = allUploadZones[uploadCounter] ? allUploadZones[uploadCounter] : null;

			if(targetUploadzone) {

				fpdInstance._addGridItemToStage(
					$item,
					{_addToUZ: targetUploadzone.uz},
					targetUploadzone.viewIndex
				);

			}

		}
		else if(addToStage) {
			fpdInstance._addGridItemToStage($item);
		}

		if(uploadCounter == totalUploadFiles-1) {
			firstUploadDone = true;
		}

	}

	var _addThumbnail = function(imgUrl, title) {

		var $thumbnail = $uploadGrid.append('<div class="fpd-item" data-title="'+title+'"><picture data-img="'+imgUrl+'"></picture><span class="fpd-price"></span><span class="fpd-icon-remove"></span></div>')
		.children('.fpd-item:last').data('source', imgUrl);

		FPDUtil.setItemPrice($thumbnail, fpdInstance);
		FPDUtil.loadGridImage($thumbnail.children('picture'), imgUrl);
		FPDUtil.createScrollbar($uploadScrollArea);

		_imageQualityRatings($thumbnail, imgUrl);

		return $thumbnail;

	};

	var _imageQualityRatings = function($thumbnail, imgUrl) {

		if(fpdInstance.mainOptions.imageQualityRatings && typeof fpdInstance.mainOptions.imageQualityRatings == 'object') {

			var low = fpdInstance.mainOptions.imageQualityRatings.low ? fpdInstance.mainOptions.imageQualityRatings.low : null,
				mid = fpdInstance.mainOptions.imageQualityRatings.mid ? fpdInstance.mainOptions.imageQualityRatings.mid : null,
				high = fpdInstance.mainOptions.imageQualityRatings.high ? fpdInstance.mainOptions.imageQualityRatings.high : null,
				icon = 'fpd-icon-star',
				iconOutline = 'fpd-icon-star-outline';

			var image = new Image();
			image.onload = function() {

				var $ratingsWrapper = $thumbnail.append('<div class="fpd-image-quality-ratings"></div>').children('.fpd-image-quality-ratings'),
					qualityLabel;

				if(low && low.length == 2) {
					var lowIcon = this.width < Number(low[0]) || this.height < Number(low[1]) ? iconOutline : icon;
					$ratingsWrapper.append('<span class="'+lowIcon+'"></span>');

					if(lowIcon == icon) {
						qualityLabel = fpdInstance.getTranslation('misc', 'image_quality_rating_low');
					}

				}

				if(mid && mid.length == 2) {
					var midIcon = this.width < Number(mid[0]) || this.height < Number(mid[1]) ? iconOutline : icon;
					$ratingsWrapper.append('<span class="'+midIcon+'"></span>');

					if(midIcon == icon) {
						qualityLabel = fpdInstance.getTranslation('misc', 'image_quality_rating_mid');
					}

				}

				if(high && high.length == 2) {
					var highIcon = this.width < Number(high[0]) || this.height < Number(high[1]) ? iconOutline : icon;
					$ratingsWrapper.append('<span class="'+highIcon+'"></span>');

					if(highIcon == icon) {
						qualityLabel = fpdInstance.getTranslation('misc', 'image_quality_rating_high');
					}


				}

				if(qualityLabel) {
					$ratingsWrapper.data('quality-label', qualityLabel)
				}

			}

			image.src = imgUrl;

		}

	};

	var _storeUploadedImage = function(url, title) {

		if(localStorageAvailable) {

			var savedLocalFiles = window.localStorage.getItem('fpd_uploaded_images') ? JSON.parse(window.localStorage.getItem('fpd_uploaded_images')) : [],
				imgObj = {
					url: url,
					title: title
				};

			savedLocalFiles.push(imgObj);
			window.localStorage.setItem('fpd_uploaded_images', JSON.stringify(savedLocalFiles))

		}

	};

	var _initFacebook = function() {

		var $albumItems = $fbAlbumDropdown.find('.fpd-dropdown-list .fpd-item');

		$fbAlbumDropdown.children('input').keyup(function() {

			$albumItems.hide();

			if(this.value.length === 0) {
				$albumItems.show();
			}
			else {
				$albumItems.filter(':containsCaseInsensitive("'+this.value+'")').show();
			}

		});

		$fbAlbumDropdown.on('click', '.fpd-dropdown-list .fpd-item', function() {

			var $this = $(this);

			$this.parent().prevAll('.fpd-dropdown-current:first').val($this.text());
			$this.siblings('.fpd-item').show();

			_selectAlbum($this.data('value'));

		});

		var _selectAlbum = function(albumID) {

			$fbGrid.empty();
			$fbAlbumDropdown.addClass('fpd-on-loading');

			FB.api('/'+albumID+'?fields=count', function(response) {

				var albumCount = response.count;

				FB.api('/'+albumID+'?fields=photos.limit('+albumCount+').fields(source,images)', function(response) {

					$fbAlbumDropdown.removeClass('fpd-on-loading');

					if(!response.error) {

						var photos = response.photos.data;

						for(var i=0; i < photos.length; ++i) {

							var photo = photos[i],
								photoLargest = photo.images[0] ? photo.images[0].source : photo.source,
								photoThumbnail = photo.images[photo.images.length-1] ? photo.images[photo.images.length-1].source : photo.source,
								$lastItem = $('<div/>', {
									'class': 'fpd-item '+lazyClass,
									'data-title': photo.id,
									'data-source': photoLargest,
									'html': '<picture data-img="'+photoThumbnail+'"></picture><span class="fpd-price"></span>'
								}).appendTo($fbGrid);

							FPDUtil.setItemPrice($lastItem, fpdInstance);

							if(lazyClass === '') {
								FPDUtil.loadGridImage($lastItem.children('picture'), photoThumbnail);
							}

						}

						FPDUtil.createScrollbar($fbScrollArea);
						FPDUtil.refreshLazyLoad($fbGrid, false);

					}

					fpdInstance.toggleSpinner(false);

				});

			});

		};

		var _fbLoggedIn = function(response) {

			if (response.status === 'connected') {
				// the user is logged in and has authenticated your app

				$module.addClass('fpd-facebook-logged-in');

				FB.api('/me/albums?fields=name,count,id', function(response) {

					var albums = response.data;
					//add all albums to select
					for(var i=0; i < albums.length; ++i) {

						var album = albums[i];
						if(album.count > 0) {
							$fbAlbumDropdown.find('.fpd-dropdown-list').append('<span class="fpd-item" data-value="'+album.id+'">'+album.name+'</span>');
						}

					}

					$albumItems = $fbAlbumDropdown.find('.fpd-dropdown-list .fpd-item');

					$fbAlbumDropdown.removeClass('fpd-on-loading');

				});

			}

		};

		if(location.protocol !== 'https:') {
			FPDUtil.log('Facebook Apps can only run in https', 'info');
			return;
		}

		$.ajaxSetup({ cache: true });
		$.getScript('//connect.facebook.com/en_US/sdk.js', function(){

			//init facebook
			FB.init({
				appId: facebookAppId,
				autoLogAppEvents: true,
				xfbml: true,
				version: 'v12.0'
			});

			FB.getLoginStatus(function(response) {
				_fbLoggedIn(response);
			});

			FB.Event.subscribe('auth.statusChange', function(response) {
				_fbLoggedIn(response);
			});

		});

	};

	//authenticate instagram
	var _authInstagram = function() {

		var popupLeft = (window.screen.width - 700) / 2,
			popupTop = (window.screen.height - 500) / 2,
			authUrl = 'https://api.instagram.com/oauth/authorize',
			authUrlQuery = {
				app_id: instagramClientId,
				redirect_uri: instagramRedirectUri,
				scope: 'user_profile,user_media',
				response_type: 'code'
			};

		var popup = window.open(authUrl+'?'+$.param(authUrlQuery), '', 'width=700,height=500,left='+popupLeft+',top='+popupTop+'');

		var interval = setInterval(function() {

			if(popup.closed) {
				clearInterval(interval);
			}

			try {

				if(popup.location && popup.location.href) {

					var url = new URL(popup.location.href),
						code = url.searchParams.get('code');

					if(code) {

						code = code.replace('#_', '');
						_instaGetAccessToken(code);
						popup.close();

					}

				}

			}
			catch(evt) {
			}


		}, 100);

	};

	//get access token
	var _instaGetAccessToken = function(code) {

		fpdInstance.toggleSpinner(true);

		$.getJSON(
			fpdInstance.mainOptions.instagramTokenUri,
			{
				code: code,
				client_app_id: instagramClientId,
				redirect_uri: instagramRedirectUri
			},
			function(data) {

				if(data) {

					if(data.access_token) {

						instaAccessToken = data.access_token;
						_loadInstaImages();

					}
					else if(data.error_message) {
						FPDUtil.log(data);
						alert(data.error_message);
						fpdInstance.toggleSpinner(false);
					}

				}
				else {
					fpdInstance.toggleSpinner(false);
				}


			}
		)

	};

	//load photos from instagram using an endpoint
	var _loadInstaImages = function(endpoint, emptyGrid) {

		endpoint = endpoint === undefined ? 'https://graph.instagram.com/me/media' : endpoint;
		emptyGrid = emptyGrid === undefined ? true : emptyGrid;

		instaLoadingStack = true;

		fpdInstance.toggleSpinner(true);

		if(emptyGrid) {
			$instaGrid.empty();
		}

		$.getJSON(
			endpoint,
			{
				fields: 'id,caption,media_url,media_type',
				access_token: instaAccessToken
			},
			function (mediaData) {

				fpdInstance.toggleSpinner(false);

				if(mediaData.data) {

		        	instaNextStack = (mediaData.paging && mediaData.paging.next) ? mediaData.paging.next : null;

		        	$.each(mediaData.data, function(i, item) {

		        		if(item.media_type !== 'VIDEO') {

			        		var image = item.media_url,
								$lastItem = $('<div/>', {
									'class': 'fpd-item '+lazyClass,
									'data-title': item.id,
									'data-source': image,
									'data-thumbnail': item.thumbnail_url ? item.thumbnail_url : item.media_url,
									'html': '<picture data-img="'+(item.thumbnail_url ? item.thumbnail_url : item.media_url)+'"></picture><span class="fpd-price"></span>'
								}).appendTo($instaGrid);

			        		FPDUtil.setItemPrice($lastItem, fpdInstance);

			        		if(lazyClass === '') {
								FPDUtil.loadGridImage($lastItem.children('picture'), image);
							}

		        		}

		            });

					if(emptyGrid) {
						FPDUtil.createScrollbar($instaScrollArea);
						FPDUtil.refreshLazyLoad($instaGrid, false);
					}

	        	}

	        	instaLoadingStack = false;

			}
		)

	};

	var _loadPixabayImages = function(query, emptyGrid) {

		if(pixabayCurrentQuery === query) {
			return false;
		}

		pixabayLoadingStack = true;

		pixabayCurrentQuery = query === undefined ? pixabayCurrentQuery : query;
		emptyGrid = emptyGrid === undefined ? true : emptyGrid;

		var perPage = 40,
			highResParam = fpdInstance.mainOptions.pixabayHighResImages ? '&response_group=high_resolution' : '',
			url = 'https://pixabay.com/api/?safesearch=true&key='+pixabayApiKey+'&page='+pixabayPage+'&per_page='+perPage+'&min_width='+fpdInstance.mainOptions.customImageParameters.minW+'&min_height='+fpdInstance.mainOptions.customImageParameters.minH+highResParam+'&q='+encodeURIComponent(pixabayCurrentQuery)+'&lang='+fpdInstance.mainOptions.pixabayLang;

		if(emptyGrid) {
			$pixabayGrid.empty();
		}

		$pixabayScrollArea.prevAll('.fpd-loader-wrapper:first').removeClass('fpd-hidden');

		$.getJSON(url, function(data) {

			$pixabayScrollArea.prevAll('.fpd-loader-wrapper:first').addClass('fpd-hidden');

			if (data.hits.length > 0) {

				data.hits.forEach(function(item) {

					var source = item.imageURL ? item.imageURL : item.webformatURL,
						$lastItem = $('<div/>', {
								'class': 'fpd-item '+lazyClass,
								'data-title': (item.id ? item.id : item.id_hash),
								'data-source': source,
								'data-thumbnail': item.webformatURL,
								'html': '<picture data-img="'+item.webformatURL+'"></picture><span class="fpd-price"></span>'
							}).appendTo($pixabayGrid);

					FPDUtil.setItemPrice($lastItem, fpdInstance);

					if(lazyClass === '') {
						FPDUtil.loadGridImage($lastItem.children('picture'), item.previewURL);
					}

					if(emptyGrid) {
						FPDUtil.createScrollbar($pixabayScrollArea);
						FPDUtil.refreshLazyLoad($pixabayGrid, false);
					}

				});

			}

			pixabayLoadingStack = false;

		})
		.fail(function(data, textStatus, jqXHR) {
			$pixabayScrollArea.prevAll('.fpd-loader-wrapper:first').addClass('fpd-hidden');
			FPDUtil.log(textStatus);
		});

	};

	var _loadDPImages = function(type, query, emptyGrid) {

		type = type === undefined ? 'search' : type;
		emptyGrid = emptyGrid === undefined ? true : emptyGrid;

		dpCurrentType = type;
		dpCurrentQuery = query;

		dpLoadingStack = true;

		var apiUrl = location.protocol+'//api.depositphotos.com?dp_apikey='+dpApiKey,
			perPage = 40;

		if(type == 'search') {
			apiUrl += '&dp_command=search&dp_search_limit=40&dp_search_offset='+dpOffset+'&dp_search_query='+encodeURIComponent(query);
			if(dpCurrentCat) {
				apiUrl += '&dp_search_categories='+encodeURIComponent(dpCurrentCat);
			}
		}
		else if(type == 'search-cats') {
			apiUrl += '&dp_command=search&dp_search_limit=40&dp_search_offset='+dpOffset+'&dp_search_query='+encodeURIComponent(query);
		}
		else if(type == 'list-cats') {
			apiUrl += '&dp_command=getCategoriesList&dp_lang='+fpdInstance.mainOptions.depositphotosLang;
		}

		if(emptyGrid) {
			$dpGrid.empty();
		}

		$dpScrollArea.prev('.fpd-head').toggleClass('fpd-cats-shown', type == 'list-cats')
		.prev('.fpd-loader-wrapper').removeClass('fpd-hidden');

		$.getJSON(apiUrl, function(data) {

			$dpScrollArea.prevAll('.fpd-loader-wrapper:first').addClass('fpd-hidden');

			var result;
			if(type == 'list-cats') {
				result = $.map(data.result, function(value, index) {
				    return [[index,value]];
				});
			}
			else {
				result = data.result;
			}

			if(result && result.length > 0) {

				result.forEach(function(item) {

					if(type == 'list-cats') {

						$('<div/>', {
							'class': 'fpd-category fpd-item',
							'data-category': item[1],
							'html': '<span>'+item[1]+'</span>'
						}).appendTo($dpGrid);

					}
					else if($dpScrollArea.prev('.fpd-cats-shown').length == 0) {

						var $lastItem = $('<div/>', {
								'class': 'fpd-item '+lazyClass,
								'data-title': item.title,
								'data-source': item.url_big,
								'data-price': fpdInstance.mainOptions.depositphotosPrice,
								'data-thumbnail': item.thumb_huge,
								'html': '<picture data-img="'+item.thumb_huge+'"></picture><span class="fpd-price"></span>'
							}).appendTo($dpGrid);

						$lastItem.data('options', {depositphotos: {id: item.id, itemURL: item.itemurl}, price: fpdInstance.mainOptions.depositphotosPrice});

						FPDUtil.setItemPrice($lastItem, fpdInstance);

						if(lazyClass === '') {
							FPDUtil.loadGridImage($lastItem.children('picture'), item.thumb_huge);
						}

					}

					if(emptyGrid) {
						FPDUtil.createScrollbar($dpScrollArea);
						FPDUtil.refreshLazyLoad($dpGrid, false);
					}

				})

			}

			dpLoadingStack = false;

		})
		.fail(function(data, textStatus, jqXHR) {
			FPDUtil.log(textStatus);
		});

	};

	_initialize();

};

var FPDTextLayersModule = {

	createList : function(fpdInstance, $container) {

		var $currentColorList,
			colorDragging = false;

		$container.off();

		//append a list item to the layers list
		var _appendLayerItem = function(element) {

			var $colorHtml = '';
			if(FPDUtil.elementHasColorSelection(element)) {

				var availableColors = FPDUtil.elementAvailableColors(element, fpdInstance),
					currentColor = '';

				if(availableColors.length > 1) {

					$colorHtml = $('<div class="fpd-color-palette fpd-grid"></div>');

					for(var i=0; i < availableColors.length; ++i) {

						var tooltipTitle = fpdInstance.mainOptions.hexNames[availableColors[i].replace('#', '').toLowerCase()];
							tooltipTitle = tooltipTitle ? tooltipTitle : availableColors[i];

						var item = '<div class="fpd-item fpd-tooltip" title="'+tooltipTitle+'" style="background-color: '+availableColors[i]+'" data-color="'+availableColors[i]+'"></div>';

						$colorHtml.append(item);
					}

					if($.isArray(element.patterns) && (FPDUtil.isSVG(element) || FPDUtil.getType(element.type) === 'text')) {

						element.patterns.forEach(function(pattern) {

							var patternTitle = pattern.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, "").replace('_', ' '),
								patternLabel = '<div class="fpd-label">'+patternTitle+'</div>';

							$colorHtml.append('<div data-pattern="'+pattern+'" style="background-image: url('+pattern+')" class="fpd-item fpd-tooltip fpd-pattern" title="'+patternTitle+'"></div>');

						})

					}

				}
				else {
					currentColor = element.fill ? element.fill : availableColors[0];
					$colorHtml = $('<div class="fpd-color-picker"><input class="fpd-current-color" type="text" value="'+currentColor+'" /></div>');
				}

			}

			var textContent = element.maxLines === 1 ? '<input type="text" value="'+element.text+'">' : '<textarea>'+element.text+'</textarea>';

			$container.find('.fpd-list').append('<div class="fpd-text-layer-item" id="'+element.id+'"><div class="fpd-title">'+element.title+'</div><div class="fpd-text-layer-content">'+textContent+'<span class="fpd-text-layer-clear">'+fpdInstance.getTranslation('modules', 'text_layers_clear')+'</span></div><div class="fpd-text-layer-meta"></div></div>');

			var $lastItem = $container.find('.fpd-text-layer-item:last').data('element', element).data('colors', availableColors),
				availableFonts = $.isArray(fpdInstance.mainOptions.fonts) ? fpdInstance.mainOptions.fonts : [];

			$lastItem.find('.fpd-text-layer-meta').append('<div class="fpd-text-layer-styles"></div>');

			if(availableFonts.length > 0) {

				$lastItem.find('.fpd-text-layer-styles').append('<div class="fpd-text-layer-font-family fpd-dropdown fpd-search"><input type="text" class="fpd-dropdown-current" value="'+element.fontFamily+'" style="font-family: '+element.fontFamily+'"><div class="fpd-dropdown-arrow"><span class="fpd-icon-arrow-dropdown"></span></div><div class="fpd-dropdown-list"><div class="fpd-scroll-area"></div></div></div>');

				availableFonts.forEach(function(fontObj, i) {

					if(typeof fontObj == 'object') {
						fontObj = fontObj.name;
					}

					$('<span/>', {
						'class': 'fpd-item',
						'data-value': fontObj,
						'html': fontObj,
						'css': {'fontFamily': fontObj},
					}).appendTo($lastItem.find('.fpd-text-layer-font-family .fpd-scroll-area'));

				});

			}

			if(element.resizable || element.__editorMode) {

				$lastItem.find('.fpd-text-layer-styles').append('<input type="number" class="fpd-text-layer-font-size fpd-tooltip" title="'+fpdInstance.getTranslation('toolbar', 'font_size')+'" value="'+element.fontSize+'" min="'+element.minFontSize+'" max="'+element.maxFontSize+'" />');

			}

			$lastItem.find('.fpd-text-layer-meta').append($colorHtml);

			FPDUtil.updateTooltip($lastItem);

		};

		//destroy all color pickers and empty list
		$container.find('.fpd-current-color').spectrum('destroy');
		$container.find('.fpd-list').empty();

		fpdInstance.getElements(fpdInstance.currentViewIndex).forEach(function(element) {

			if(FPDUtil.elementIsEditable(element) && FPDUtil.getType(element.type) === 'text') {
				_appendLayerItem(element);
			}

		});

		FPDUtil.createScrollbar($container.find('.fpd-text-layers-panel .fpd-scroll-area'));

		//Color Picker
		$container.find('input.fpd-current-color').spectrum('destroy').spectrum({
			flat: false,
			preferredFormat: "hex",
			showInput: true,
			showInitial: true,
			showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
			palette: fpdInstance.mainOptions.colorPickerPalette,
			showButtons: false,
			show: function(color) {

				FPDUtil.spectrumColorNames($(this).spectrum('container'), fpdInstance);

				var element = $(this).parents('.fpd-text-layer-item:first').data('element');
				element._tempFill = color.toHexString();

			},
			move: function(color) {

				var element = $(this).parents('.fpd-text-layer-item:first').data('element');
				//only non-png images are chaning while dragging
				if(colorDragging === false || FPDUtil.elementIsColorizable(element) !== 'png') {
					fpdInstance.currentViewInstance.changeColor(element, color.toHexString());
				}

			},
			change: function(color) {

				$(document).unbind("click.spectrum"); //fix, otherwise change is fired on every click
				var element = $(this).parents('.fpd-text-layer-item:first').data('element');
				fpdInstance.currentViewInstance.setElementParameters({fill: color.toHexString()}, element);

			}
		})
		.on('beforeShow.spectrum', function(e, tinycolor) {
			if($currentColorList) {
				$currentColorList.remove();
				$currentColorList = null;
			}
		})
		.on('dragstart.spectrum', function() {
			colorDragging = true;
		})
		.on('dragstop.spectrum', function(evt, color) {
			colorDragging = false;
			var element = $(this).parents('.fpd-text-layer-item:first').data('element');
			fpdInstance.currentViewInstance.changeColor(element, color.toHexString());
		});

		//select color from color palette
		$container.on('click', '.fpd-color-palette .fpd-item', function(evt) {

			evt.stopPropagation();

			var $this = $(this),
				element = $this.parents('.fpd-text-layer-item').data('element'),
				fill = $this.data('pattern') ? $this.data('pattern') : $this.data('color'),
				paramsObj = $this.data('pattern') ? {pattern: fill} : {fill: fill};

			fpdInstance.deselectElement();
			fpdInstance.currentViewInstance.setElementParameters(paramsObj, element);

		});

		$container.on('click', '.fpd-text-layer-clear', function(evt) {

			evt.stopPropagation();

			var $this = $(this),
				$layerItem = $this.parents('.fpd-text-layer-item'),
				element = $layerItem.data('element');

			fpdInstance.currentViewInstance.setElementParameters({text: ''}, element);
			$layerItem.find('.fpd-text-layer-content > *').val('');

		});

		$container
		.on('keyup', 'textarea, .fpd-text-layer-content input[type="text"]',function(evt) {

			evt.stopPropagation();

			var $this = $(this),
				element = $this.parents('.fpd-text-layer-item').data('element');

			//remove emojis
			if(fpdInstance.mainOptions.disableTextEmojis) {
				this.value = this.value.replace(FPDEmojisRegex, '');
				this.value = this.value.replace(String.fromCharCode(65039), ""); //fix: some emojis left a symbol with char code 65039
			}

			fpdInstance.currentViewInstance.setElementParameters({text: this.value}, element);

		})
		.on('change', '.fpd-text-layer-font-size', function() {

			var $this = $(this),
				element = $this.parents('.fpd-text-layer-item').data('element');

			fpdInstance.currentViewInstance.setElementParameters({fontSize: parseInt(this.value)}, element);

			this.value = element.fontSize;

		})
		.on('click', '.fpd-text-layer-font-family.fpd-dropdown', function() {
			$(this).toggleClass('fpd-active');
		})
		.on('keyup', '.fpd-text-layer-font-family .fpd-dropdown-current', function() {

			var $fontItems = $(this).nextAll('.fpd-dropdown-list').find('.fpd-item');
			$fontItems.hide();

			if(this.value.length === 0) {
				$fontItems.show();
			}
			else {
				$fontItems.filter(':containsCaseInsensitive("'+this.value+'")').show();
			}

		})
		.on('click touchend', '.fpd-text-layer-font-family .fpd-item', function(evt) {

			var $this = $(this),
				element = $this.parents('.fpd-text-layer-item').data('element');

			$this.parents('.fpd-dropdown').children('.fpd-dropdown-current').val($this.text());
			fpdInstance.currentViewInstance.setElementParameters({fontFamily: $this.data('value')}, element);

		})

		var _textChanged = function(evt, element, parameters) {

			if(parameters.text) {

				if(document.activeElement && $(document.activeElement).parent('.fpd-text-layer-content').length) {
					return;
				}

				$container.find('.fpd-list')
				.find('[id="'+element.id+'"] textarea, [id="'+element.id+'"] .fpd-text-layer-content input[type="text"]').val(parameters.text);

			}
			else if(parameters.fontSize) {
				$container.find('.fpd-list')
				.find('[id="'+element.id+'"] .fpd-text-layer-font-size').val(parameters.fontSize);
			}
			else if(parameters.fontFamily) {
				$container.find('.fpd-list')
				.find('[id="'+element.id+'"] .fpd-text-layer-font-family > .fpd-dropdown-current')
				.val(parameters.fontFamily)
				.css('font-family', parameters.fontFamily);
			}

		};

		//text is changed in canvas
		fpdInstance.$container.off('elementModify', _textChanged);
		fpdInstance.$container.on('elementModify', _textChanged);

		//element color change
		var _elementColorChanged = function(evt, element, hex, colorLinking) {

			var $currentColor = $container.find('.fpd-list')
			.find('[id="'+element.id+'"] .fpd-current-color');

			if($currentColor.is('input')) {
				$currentColor.spectrum('set', hex);
			}
			else {
				$currentColor.css('background', hex);
			}

		};

		fpdInstance.$container.off('elementColorChange', _elementColorChanged);
		fpdInstance.$container.on('elementColorChange', _elementColorChanged);

	}

};



var FPDLayoutsModule = function(fpdInstance, $module) {

	'use strict';

	$ = jQuery;

	var instance = this,
		currentLayouts = [],
		_layoutElementLoadingIndex = 0,
		_totalLayoutElements = 0,
		$scrollArea = $module.find('.fpd-scroll-area');

	var _setupLayouts = function(layouts) {

		if($.isArray(layouts)) {

			currentLayouts = layouts;

			$scrollArea.find('.fpd-grid').empty();

			layouts.forEach(function(layoutObject) {

				var $lastItem = $('<div/>', {
							'class': 'fpd-item fpd-tooltip',
							'title': layoutObject.title,
							'html': '<picture style="background-image: url('+layoutObject.thumbnail+'");"></picture>'
						}).appendTo($scrollArea.find('.fpd-grid'));

			});

			FPDUtil.updateTooltip($scrollArea.find('.fpd-grid'));
			FPDUtil.createScrollbar($scrollArea);

		}

	};

	var _loadingLayoutElement = function(evt, type, source, title, params) {

		_layoutElementLoadingIndex++;

		var loadElementState = title + '<br>' + String(_layoutElementLoadingIndex) + '/' + _totalLayoutElements;
		fpdInstance.$container.find('.fpd-loader-text').html(loadElementState);

	};

	var _initialize = function() {

		fpdInstance.$container
		.on('productCreate', function(evt, views) {

			if(views.length > 0) {
				_setupLayouts(views[0].options.layouts)
			}

		});

		$scrollArea.on('click', '.fpd-item', function() {

			if(fpdInstance.productCreated) {

				var $confirm = FPDUtil.showModal(fpdInstance.getTranslation('modules', 'layouts_confirm_replacement'), false, 'confirm', fpdInstance.$modalContainer),
					layoutIndex = $scrollArea.find('.fpd-item').index($(this));

				$confirm.find('.fpd-confirm').text(fpdInstance.getTranslation('modules', 'layouts_confirm_button')).click(function() {

					_layoutElementLoadingIndex = 0;

					var $viewInstance = $(fpdInstance.currentViewInstance);

					_totalLayoutElements = currentLayouts[layoutIndex].elements.length;

					fpdInstance.globalCustomElements = [];
					if(fpdInstance.mainOptions.replaceInitialElements) {
						fpdInstance.globalCustomElements = fpdInstance.getCustomElements();
					}

					$viewInstance.on('beforeElementAdd', _loadingLayoutElement);

					fpdInstance.toggleSpinner(true);
					fpdInstance.currentViewInstance.loadElements(currentLayouts[layoutIndex].elements, function() {

						fpdInstance.toggleSpinner(false);
						$viewInstance.off('beforeElementAdd', _loadingLayoutElement);

						/**
						 * Gets fired when a all elements of layout are added.
						 *
						 * @event FancyProductDesigner#productAdd
						 * @param {Event} event
						 * @param {Array} elements - Added elements.
						 */
						fpdInstance.$container.trigger('layoutElementsAdded', [currentLayouts[layoutIndex].elements]);

					});

					fpdInstance.$viewSelectionWrapper.find('.fpd-item').eq(fpdInstance.currentViewIndex).children('picture').css('background-image', 'url('+currentLayouts[layoutIndex].thumbnail+')');

					$confirm.find('.fpd-modal-close').click();

				});

			}

		});

	};

	_initialize();

};

var FPDToolbarSide = function($uiElementToolbar, fpdInstance) {

	'use strict';

	$ = jQuery;

	var instance = this,
		$body = $('body'),
		$uiToolbarSub = $uiElementToolbar.children('.fpd-sub-panel'),
		$colorPicker = $uiElementToolbar.find('.fpd-color-wrapper'),
		colorDragging = false,
		resetScroll = true,
		subPanelResetted = true;

	this.isTransforming = false; //is true, while transforming via slider
	this.placement = fpdInstance.mainOptions.toolbarPlacement;

	var _initialize = function() {

		$uiElementToolbar.addClass('fpd-ui-theme-'+fpdInstance.mainOptions.uiTheme)

		instance.setPlacement(fpdInstance.mainOptions.toolbarPlacement);

		$body.on('mousedown touchstart', function(evt) {

			if(typeof evt.target.className.includes === 'function' && evt.target.className.includes('fpd-range-slider')) {
				instance.isTransforming = true;
				evt.preventDefault();
			}

		})
		.on('touchmove', function(evt) {
			if(evt.target.className.includes('fpd-range-slider')) {
				evt.preventDefault();
			}
		})
		.on('mouseup touchend', function() {
			instance.isTransforming = false;
		});

		//prevent parent scrolling
		var $scrollArea = $uiElementToolbar.children('.fpd-scroll-area');

		if(!fpdInstance.$container.hasClass('fpd-device-desktop')) {

			$uiElementToolbar.children('.fpd-scroll-area').on('mousewheel', function(evt, d) {

			    if(
			    	(this.scrollLeft === ( $scrollArea.get(0).scrollWidth -  $scrollArea.width()) && d < 0)
			    	|| (this.scrollLeft === 0 && d > 0)
			    ) {
			    	evt.preventDefault();
			    }

			});

		}

		fpdInstance.$container.on('screenSizeChange', function(evt, device) {

			if(device == 'desktop') {

				instance.setPlacement('mainbar');

			}
			else {
				instance.setPlacement('body');
			}

			_createScrollbar($uiElementToolbar.find('.fpd-sub-panel .fpd-scroll-area'));

		});

		//close toolbar
		$uiElementToolbar
		//close sub panel
		.on('click', '.fpd-close-sub-panel', function() {

			$uiElementToolbar.find('.fpd-panel-font-family input').val('').keyup();
			$colorPicker.children('input').spectrum('hide');

			if(fpdInstance.currentElement && fpdInstance.currentElement.exitEditing === 'function') {
				fpdInstance.currentElement.exitEditing();
			}

			$uiElementToolbar.removeClass('fpd-panel-visible');

			fpdInstance.$container.trigger('toolbarPanelClose');

		})
		//deselect element
		.on('click', '.fpd-close-panel', function() {

			$uiElementToolbar.find('.fpd-panel-font-family input').val('').keyup();
			$colorPicker.children('input').spectrum('hide');

			if(fpdInstance.currentElement && fpdInstance.currentElement.exitEditing === 'function') {
				fpdInstance.currentElement.exitEditing();
			}

			fpdInstance.deselectElement();

			fpdInstance.$container.trigger('toolbarPanelClose');

		});

		//set max values
		var maxValuesKeys = Object.keys(fpdInstance.mainOptions.maxValues);
		for(var i=0; i < maxValuesKeys.length; ++i) {

			var maxValueProp = maxValuesKeys[i];

			$uiElementToolbar.find('[data-control="'+maxValueProp+'"]')
			.attr('max', fpdInstance.mainOptions.maxValues[maxValueProp]);

		}

		//tool nav items
		$uiElementToolbar.find('.fpd-tools-nav > div').click(function() {

			subPanelResetted = false;

			var $this = $(this);

			if($this.data('panel')) { //has a sub a panel

				var element = fpdInstance.currentElement;

				//add active state to nav item
				$this.addClass('fpd-active')
				.siblings().removeClass('fpd-active');

				//display related tool panel
				$uiToolbarSub
				.children().removeClass('fpd-active') //hide all panels in sub wrapper
				.filter('.fpd-panel-'+$this.data('panel')).addClass('fpd-active'); //display related panel

				$uiElementToolbar.addClass('fpd-panel-visible')
				.attr('data-fields', $this.data('fields') ? $this.data('fields') : '');

				if($this.data('panel') == 'color') {

					if(FPDUtil.elementHasColorSelection(element)) {

						var availableColors;
						if(element.__editorMode) {
							availableColors = ['#000'];
						}
						else if(element.colorLinkGroup) {
							availableColors = fpdInstance.colorLinkGroups[element.colorLinkGroup].colors;
						}
						else {
							availableColors = element.colors;
						}

						$colorPicker.children('input').spectrum('destroy');
						$colorPicker.empty().removeClass('fpd-colorpicker-group');

						//svg with more than one path
						if(element.type == FPDPathGroupName && (element.getObjects().length > 1 || availableColors == 1 || element.__editorMode)) {

							var paths = element.getObjects();
							for(var i=0; i < paths.length; ++i) {
								var path = paths[i],
									color = tinycolor(path.fill);

								$colorPicker.append('<input type="text" value="'+color.toHexString()+'" />');
							}

							$colorPicker.addClass('fpd-colorpicker-group').children('input').spectrum('destroy').spectrum({
								showPaletteOnly: $.isArray(element.colors) && !element.__editorMode,
								preferredFormat: "hex",
								showInput: true,
								showInitial: true,
								showButtons: false,
								showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
								palette: $.isArray(element.colors) ? element.colors : fpdInstance.mainOptions.colorPickerPalette,
								containerClassName: 'fpd-theme-'+fpdInstance.mainOptions.toolbarTheme,
								show: function(color) {

									var svgColors = FPDUtil.changePathColor(
										fpdInstance.currentElement,
										$colorPicker.children('input').index(this),
										color
									);

									FPDUtil.spectrumColorNames($(this).spectrum('container'), fpdInstance);

									element._tempFill = svgColors;

								},
								move: function(color) {

									var svgColors = FPDUtil.changePathColor(
										element,
										$colorPicker.children('input').index(this),
										color
									);

									fpdInstance.currentViewInstance.setElementParameters({fill: svgColors}, element);

								},
								change: function(color) {

									var svgColors = FPDUtil.changePathColor(
										element,
										$colorPicker.children('input').index(this),
										color
									);

									$(document).unbind("click.spectrum"); //fix, otherwise change is fired on every click
									fpdInstance.currentViewInstance.setElementParameters({fill: svgColors}, element);

								}
							});

						}
						//color list or for svg with one path
						else if(availableColors.length > 1 || (element.type == FPDPathGroupName && element.getObjects().length === 1)) {

							$colorPicker.html('<div class="fpd-scroll-area"><div class="fpd-color-palette fpd-grid"></div></div>');

							for(var i=0; i < availableColors.length; ++i) {

								var color = availableColors[i],
									colorName = fpdInstance.mainOptions.hexNames[color.replace('#', '').toLowerCase()];

								colorName = colorName ? colorName : color;
								$colorPicker.find('.fpd-grid').append('<div class="fpd-item fpd-tooltip" title="'+colorName+'" style="background-color: '+color+';"></div>')
								.children('.fpd-item:last').click(function() {

									var color = tinycolor($(this).css('backgroundColor'));

									$uiElementToolbar.find('.fpd-current-fill').css('background', color.toHexString());

									var fillValue = color.toHexString();
									if(fpdInstance.currentElement.type == FPDPathGroupName) {

										fillValue = FPDUtil.changePathColor(
											element,
											0,
											color
										);

									}

									fpdInstance.currentViewInstance.setElementParameters({fill: fillValue});

								});

							}

							_createScrollbar($colorPicker.children('.fpd-scroll-area'));
							FPDUtil.updateTooltip($colorPicker);

						}
						//colorwheel
						else {

							$colorPicker.html('<input type="text" value="'+(element.fill ? element.fill : availableColors[0])+'" />');

							$colorPicker.children('input').spectrum({
								flat: true,
								preferredFormat: "hex",
								showInput: true,
								showInitial: true,
								showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
								palette: fpdInstance.mainOptions.colorPickerPalette,
								containerClassName: 'fpd-theme-'+fpdInstance.mainOptions.toolbarTheme,
								allowEmpty: Boolean(element.__editorMode),
								show: function(color) {

									FPDUtil.spectrumColorNames($(this).spectrum('container').next('.sp-container'), fpdInstance);
									element._tempFill = color.toHexString();

								},
								move: function(color) {

									//only non-png images are changing while dragging
									if(colorDragging === false || FPDUtil.elementIsColorizable(element) !== 'png') {
										_setElementColor(color ? color.toHexString() : '');
									}

								},
								change: function(color) {

									$(document).unbind("click.spectrum"); //fix, otherwise change is fired on every click
									fpdInstance.currentViewInstance.setElementParameters({fill: color ? color.toHexString() : false}, element);

								}
							})
							.on('dragstart.spectrum', function() {
								colorDragging = true;
							})
							.on('dragstop.spectrum', function(evt, color) {
								colorDragging = false;
								_setElementColor(color.toHexString());
							});

						}
					}

					//patterns
					if((FPDUtil.isSVG(element) || FPDUtil.getType(element.type) === 'text') && element.patterns && element.patterns.length) {

						$uiToolbarSub.find('.fpd-tool-patterns .fpd-grid').empty();
						for(var i=0; i < element.patterns.length; ++i) {

							var patternUrl = element.patterns[i],
								$lastItem = $('<div/>', {
									'class': 'fpd-item',
									'data-pattern': patternUrl,
									'html': '<picture style="background-image: url('+patternUrl+');"></picture>'
								}).appendTo($uiToolbarSub.find('.fpd-tool-patterns .fpd-grid'));

							$lastItem.click(function() {

								var patternUrl = $(this).data('pattern');
								$uiElementToolbar.find('.fpd-current-fill').css('background', 'url('+patternUrl+')');
								fpdInstance.currentViewInstance.setElementParameters( {pattern: patternUrl} );


							});

						}

						_createScrollbar($uiToolbarSub.find('.fpd-tool-patterns .fpd-scroll-area'));

					}

					//stroke color
				    $uiToolbarSub.find('.fpd-stroke-color-picker input').spectrum('destroy').spectrum({
					    color: element.stroke ? element.stroke : '#000',
						flat: true,
						preferredFormat: "hex",
						showInput: true,
						showInitial: true,
						palette: element.strokeColors,
						showPaletteOnly: element.strokeColors && element.strokeColors.length > 0,
						containerClassName: 'fpd-theme-'+fpdInstance.mainOptions.toolbarTheme,
						move: function(color) {
							instance.isTransforming = true;
							fpdInstance.currentViewInstance.setElementParameters( {stroke: color.toHexString()} );

						},
						change: function(color) {

							fpdInstance.currentViewInstance.setElementParameters({stroke: color.toHexString()});

						}
					});


					//shadow color
				    $uiToolbarSub.find('.fpd-shadow-color-picker input').spectrum('destroy').spectrum({
					    color: element.stroke ? element.stroke : '#000',
						flat: true,
						preferredFormat: "hex",
						showInput: true,
						showInitial: true,
						allowEmpty: true,
						showPalette: fpdInstance.mainOptions.colorPickerPalette && fpdInstance.mainOptions.colorPickerPalette.length > 0,
						palette: fpdInstance.mainOptions.colorPickerPalette,
						containerClassName: 'fpd-theme-'+fpdInstance.mainOptions.toolbarTheme,
						move: function(color) {

							if(color) {
								instance.isTransforming = true;
								fpdInstance.currentViewInstance.setElementParameters( {shadowColor: color.toHexString()} );
							}

						},
						change: function(color) {

							fpdInstance.currentViewInstance.setElementParameters({shadowColor: color ? color.toHexString() : ''});

						}
					});

				}

				instance.toggle();
				$uiToolbarSub.find('.fpd-slider-number').change();
				FPDUtil.createScrollbar($uiToolbarSub.find('.fpd-active .fpd-scroll-area'));

				fpdInstance.$container.trigger('toolbarPanelOpen', [$this.data('panel')]);

				subPanelResetted = true;

			}

		});

		//call content in tab
		$uiToolbarSub.find('.fpd-panel-tabs > span').click(function() {

			var $this = $(this);

			$this.addClass('fpd-active').siblings().removeClass('fpd-active');
			var $activePanel = $this.parent().siblings('.fpd-panel-tabs-content').children('[data-id="'+this.dataset.tab+'"]').addClass('fpd-active');

			$activePanel.siblings().removeClass('fpd-active');
			$activePanel.find('.fpd-slider-range').rangeslider('update', true, false);

		});

		//create range slider
		var saveUndo = true;
		$uiToolbarSub.find('.fpd-slider-range').rangeslider({
			polyfill: false,
			rangeClass: 'fpd-range-slider',
			disabledClass: 'fpd-range-slider--disabled',
			horizontalClass: 'fpd-range-slider--horizontal',
		    verticalClass: 'fpd-range-slider--vertical',
		    fillClass: 'fpd-range-slider__fill',
		    handleClass: 'fpd-range-slider__handle',
		    onSlide: function(pos, value) {

				instance.isTransforming = true;

			    if(fpdInstance.currentViewInstance && fpdInstance.currentViewInstance.currentElement && instance.isTransforming) {

				    if(!this.$element.is(':visible')) {
					    return;
				    }

				    var props = {},
				    	propKey = this.$element.data('control');

				    props[propKey] = value;

				     //proportional scaling
				    if(propKey === 'scaleX' && fpdInstance.currentElement && fpdInstance.currentElement.lockUniScaling) {
					    props.scaleY = value;
				    }

					//fix: text vanished when autoselected
				    if((propKey == 'scaleX' && value == 0) || (propKey == 'scaleY' && value == 0)) {
					    return;
				    }

				    fpdInstance.currentViewInstance.setElementParameters(
						props,
						fpdInstance.currentViewInstance.currentElement,
						saveUndo
					);

					this.$element.parent().siblings('.fpd-slider-number').val(value);
					saveUndo = false;

			    }

		    },
		    onSlideEnd: function(pos, value) {

			    if(this.$element.data('control') === 'scaleX' && fpdInstance.currentElement && fpdInstance.currentElement.lockUniScaling) {

					$uiElementToolbar.find('[data-control="scaleY"]').val(value)
					.filter('.fpd-slider-range').rangeslider('update', true, false);

				}

			    instance.isTransforming = false;
			    saveUndo = true;

		    }

		});

		//Button with mulitple options
		$uiElementToolbar.on('click', '.fpd-btn-options', function(evt) {

			evt.preventDefault();

			var $this = $(this),
				options = $this.data('options'),
				optionKeys = Object.keys(options),
				currentVal = fpdInstance.currentElement ? fpdInstance.currentElement[this.dataset.control] : optionKeys[0],
				nextOption = optionKeys.indexOf(currentVal) == optionKeys.length - 1 ? optionKeys[0] : optionKeys[optionKeys.indexOf(currentVal)+1],
				params = {};

			params[this.dataset.control] = nextOption;
			$this.children('span:first').removeClass().addClass(options[nextOption]);
			fpdInstance.currentViewInstance.setElementParameters(params);


		});

		$uiElementToolbar.find('.fpd-toggle').click(function() {

			var $this = $(this).toggleClass('fpd-enabled'),
				toggleParameters = {};

			//ignore curved text switcher
			if(!$this.hasClass('fpd-curved-text-switcher')) {

				toggleParameters[$this.data('control')] = $this.hasClass('fpd-enabled') ? $this.data('enabled') : $this.data('disabled');

				if($this.hasClass('fpd-tool-uniscaling-locker')) {
					_lockUniScaling($this.hasClass('fpd-enabled'));
				}

				fpdInstance.currentViewInstance.setElementParameters(toggleParameters);

			}

		});

		$uiElementToolbar.find('.fpd-number').change(function() {

			var $this = $(this),
				numberParameters = {};

			if( this.value > Number($this.attr('max')) ) {
				this.value = Number($this.attr('max'));
			}

			if( this.value < Number($this.attr('min')) ) {
				this.value = Number($this.attr('min'));
			}

			var value = Number(this.value);

			if($this.hasClass('fpd-slider-number')) {

				$this.siblings('.fpd-range-wrapper').children('input').val(this.value)
				.rangeslider('update', true, false);

				if($this.data('control') === 'scaleX' && fpdInstance.currentElement && fpdInstance.currentElement.lockUniScaling) {
					$uiElementToolbar.find('[data-control="scaleY"]').val(value).change();
				}

			}

			numberParameters[$this.data('control')] = value;

			if(subPanelResetted && fpdInstance.currentViewInstance && $(document.activeElement).is(':not(textarea)')) {

				fpdInstance.currentViewInstance.setElementParameters(
					numberParameters,
					fpdInstance.currentViewInstance.currentElement
				);

			}

		});


		//append fonts to dropdown
		if(fpdInstance.mainOptions.fonts && fpdInstance.mainOptions.fonts.length > 0) {

			var $fontsList =  $uiToolbarSub.find('.fpd-fonts-list');
			for(var i=0; i < fpdInstance.mainOptions.fonts.length; ++i) {

				var font = fpdInstance.mainOptions.fonts[i],
					fontName = font;

				if(typeof font == 'object') {
					fontName = font.name;
				}

				$('<span/>', {
					'class': 'fpd-item',
					'data-value': fontName,
					'html': fontName,
					'css': {'fontFamily': fontName}
				}).appendTo($fontsList);

			}

			$uiToolbarSub
			.on('keyup', '.fpd-panel-font-family input', function() {

				var $items = $(this).css('font-family', 'Helvetica').next('.fpd-scroll-area')
				.find('.fpd-fonts-list .fpd-item').hide();

				if(this.value.length === 0) {
					$items.show();
				}
				else {
					$items.filter(':containsCaseInsensitive("'+this.value+'")').show();
				}

			})
			.on('click', '.fpd-fonts-list .fpd-item', function() {

				var selectedFont = this.dataset.value;

				$uiElementToolbar
				.find('.fpd-fonts-list .fpd-item').removeClass('fpd-active')
				.filter('[data-value="'+selectedFont+'"]').addClass('fpd-active')

				fpdInstance.currentViewInstance.setElementParameters({fontFamily: selectedFont});

			});

		}

	    $uiToolbarSub.find('textarea[data-control="text"]')
	    .on('keyup', function(evt) {

		    evt.stopPropagation;
		    evt.preventDefault();

		    var selectionStart = this.selectionStart,
			 	selectionEnd = this.selectionEnd;

		    fpdInstance.currentViewInstance.setElementParameters( {text: this.value} );

		    this.selectionStart = selectionStart;
			this.selectionEnd = selectionEnd;

	    });

		//advanced editing: crop, filters, color manipulation
		$uiElementToolbar.find('.fpd-tool-advanced-editing').click(function() {

			if(fpdInstance.currentViewInstance && fpdInstance.currentViewInstance.currentElement && fpdInstance.currentViewInstance.currentElement.source) {

				var source = fpdInstance.currentViewInstance.currentElement.source,
					$modal = FPDUtil.showModal($(fpdInstance.translatedUI).children('.fpd-image-editor-container').clone(), true),
					imageEditor = new FPDImageEditor(
						$modal.find('.fpd-image-editor-container'),
						fpdInstance.currentViewInstance.currentElement,
						fpdInstance
					);

				imageEditor.loadImage(source);

			}

		});

		//Position Tools
		$uiToolbarSub.on('click', '.fpd-icon-button-group > span', function() {

			var $this = $(this);

			if($this.hasClass('fpd-align-left')) {
				fpdInstance.currentViewInstance.alignElement('left');
			}
			else if($this.hasClass('fpd-align-top')) {
				fpdInstance.currentViewInstance.alignElement('top');
			}
			else if($this.hasClass('fpd-align-right')) {
				fpdInstance.currentViewInstance.alignElement('right');
			}
			else if($this.hasClass('fpd-align-bottom')) {
				fpdInstance.currentViewInstance.alignElement('bottom');
			}
			else if($this.hasClass('fpd-align-center-h')) {
				fpdInstance.currentViewInstance.centerElement(true, false);
			}
			else if($this.hasClass('fpd-align-center-v')) {
				fpdInstance.currentViewInstance.centerElement(false, true);
			}
			else if($this.hasClass('fpd-flip-h')) {
				fpdInstance.currentViewInstance.setElementParameters({flipX: !fpdInstance.currentElement.get('flipX')});
			}
			else if($this.hasClass('fpd-flip-v')) {
				fpdInstance.currentViewInstance.setElementParameters({flipY: !fpdInstance.currentElement.get('flipY')});
			}

		});

		//move layer position
		$uiElementToolbar.find('.fpd-tool-layer-up, .fpd-tool-layer-down').click(function() {

			var currentZ = fpdInstance.currentViewInstance.getZIndex();

			currentZ = $(this).hasClass('fpd-tool-layer-up') ? currentZ+1 : currentZ-1;
			currentZ = currentZ < 0 ? 0 : currentZ;

			fpdInstance.currentViewInstance.setElementParameters( {z: currentZ} );

	    });

		//reset element
	    $uiElementToolbar.find('.fpd-tool-reset').click(function() {

		    $(document).unbind("click.spectrum"); //needs to be triggered, otherwise color is not resetted
			$uiElementToolbar.find('.tooltipstered').tooltipster('destroy');

			var originParams = fpdInstance.currentElement.originParams;
			delete originParams['clipPath'];

			//if element has bounding box, rescale for scale mode
			if(fpdInstance.currentElement.boundingBox) {
				fpdInstance.currentElement.scaleX = 1;
				originParams.boundingBox = fpdInstance.currentElement.boundingBox;
			}

		    fpdInstance.currentViewInstance.setElementParameters( fpdInstance.currentElement.originParams );
		    fpdInstance.currentViewInstance.deselectElement();

		});

		//duplicate element
	    $uiElementToolbar.find('.fpd-tool-duplicate').click(function() {

		    $(document).unbind("click.spectrum"); //needs to be triggered, otherwise color is not resetted
			$uiElementToolbar.find('.tooltipstered').tooltipster('destroy');

		    fpdInstance.currentViewInstance.duplicate();

		});

		//remove element
	    $uiElementToolbar.find('.fpd-tool-remove').click(function() {

		    $(document).unbind("click.spectrum"); //needs to be triggered, otherwise color is not resetted
			$uiElementToolbar.find('.tooltipstered').tooltipster('destroy');

		    fpdInstance.currentViewInstance.removeElement(fpdInstance.currentViewInstance.currentElement);

		});

		fpdInstance.$container.on('elementModify', function(evt, element, parameters) {

			if(parameters.fontSize) {
				$uiElementToolbar.find('.fpd-tool-text-size > .fpd-current-val')
				.text(parseInt (parameters.fontSize));
			}

		});

		FPDUtil.updateTooltip($uiElementToolbar);

	};

	var _createScrollbar = function($target) {

		$target
		.mCustomScrollbar("destroy")
		.mCustomScrollbar({
			axis: fpdInstance.$container.hasClass('fpd-device-desktop') ? 'y' : 'x',
			scrollInertia: 200,
			documentTouchScroll: false,
			contentTouchScroll: true,
			mouseWheel: {
				enable: true,
				preventDefault: true
			},
			advanced:{
	        	autoExpandHorizontalScroll: true
	      	}
		});

	};

	var _toggleNavItem = function(tool, showHide, fields) {

		showHide = showHide === undefined ? true : showHide;

		var $tools = $uiElementToolbar.find('.fpd-tools-nav > .fpd-tool-'+tool);

		if(fields) {
			$tools = $tools.filter('[data-fields="'+fields+'"]');
		}

		return $tools.toggleClass('fpd-hidden', !showHide);

	};

	var _togglePanelTool = function(panel, tool, showHide) {

		showHide = Boolean(showHide);

		return $uiToolbarSub.children('.fpd-panel-'+panel)
		.find('.fpd-tool-'+tool).toggleClass('fpd-hidden', !showHide)
		.find('[class^="fpd-tool-"],div[class*=" fpd-tool-"]')
		.toggleClass('fpd-hidden', !showHide)

	};

	var _togglePanelTab = function(panel, tab, showHide) {

		$uiToolbarSub.children('.fpd-panel-'+panel)
		.find('.fpd-panel-tabs [data-tab="'+tab+'"]')
		.toggleClass('fpd-disabled', !showHide);

	};

	var _setElementColor = function(color) {

		$uiElementToolbar.find('.fpd-current-fill').css('background', color);
		fpdInstance.currentViewInstance.changeColor(fpdInstance.currentViewInstance.currentElement, color);

	};

	var _lockUniScaling = function(toggle) {

		 $uiToolbarSub.find('.fpd-tool-uniscaling-locker > span').removeClass()
		 .addClass(toggle ? 'fpd-icon-locked' : 'fpd-icon-unlocked');

		 $uiToolbarSub.find('.fpd-tool-scaleY').toggleClass('fpd-disabled', toggle);

	};

	this.update = function(element) {

		this.hideTools();
		$uiElementToolbar.removeClass('fpd-type-image');

		var source = element.source,
			allowedImageTypes = [
				'png',
				'jpg',
				'jpeg',
				'svg'
			];

		if(source) {
			source = source.split('?')[0];//remove all url parameters
			var imageParts = source.split('.'),
				sourceExt = imageParts.pop().toLowerCase();

		}

		//COLOR: colors array, true=svg colorization
		if(FPDUtil.elementHasColorSelection(element)) {

			_toggleNavItem('color');
			_togglePanelTab('color', 'fill', true);
			$colorPicker.removeClass('fpd-hidden');

		}

		if((FPDUtil.isSVG(element) || FPDUtil.getType(element.type) === 'text') && element.patterns && element.patterns.length) {

			_toggleNavItem('color');

			_togglePanelTab('color', 'fill', true);
			_togglePanelTool('color', 'patterns', true);

		}
		else {
			_togglePanelTool('color', 'patterns', false);
		}

		//TRANSFORM
		var showScale = Boolean((element.resizable && FPDUtil.getType(element.type) === 'image') || element.uniScalingUnlockable || element.__editorMode);

		if(showScale || element.rotatable || element.draggable || element.zChangeable || element.__editorMode) {

			_toggleNavItem('transform');
			_toggleNavItem('layer-depth', Boolean(element.zChangeable || element.__editorMode));

			if(!fpdInstance.$container.hasClass('fpd-device-desktop')) {
				_toggleNavItem('transform', element.rotatable || showScale, 'transform');
			}

			_togglePanelTool('transform', 'scale', showScale);
			_lockUniScaling(element.lockUniScaling);
			_togglePanelTool('transform', 'uniscaling-locker', Boolean(element.uniScalingUnlockable || element.__editorMode));
			_togglePanelTool('transform', 'angle', Boolean(element.rotatable || element.__editorMode));
			_togglePanelTool('transform', 'position', Boolean(element.draggable || element.__editorMode));
			_togglePanelTool('transform', 'flip', Boolean(element.draggable || element.__editorMode));
			_togglePanelTool('transform', 'layer-depth', Boolean(element.zChangeable || element.__editorMode));

		}

		//EDIT TEXT
		if(FPDUtil.getType(element.type) === 'text' && (element.editable || element.__editorMode)) {

			_toggleNavItem('edit-text');
			_toggleNavItem('font-family');
			_toggleNavItem('text-bold');
			_toggleNavItem('text-italic');
			_toggleNavItem('text-underline');
			_toggleNavItem('text-align');
			_toggleNavItem('text-transform');

			_togglePanelTool('edit-text', 'text-styles', true);
			_togglePanelTool('edit-text', 'text-size', Boolean(element.resizable || element.__editorMode));
			_togglePanelTool('edit-text', 'line-height', Boolean(element.resizable || element.__editorMode));
			_togglePanelTool('edit-text', 'letter-spacing', Boolean(element.resizable || element.__editorMode));

			_togglePanelTab('color', 'stroke', true);
			_togglePanelTab('color', 'shadow', true);

			if(element.curvable && !element.textBox) {
				_toggleNavItem('curved-text');
			}

			$uiElementToolbar.find('textarea[data-control="text"]').val(element.get('text'));

			_toggleNavItem('edit-text', !element.textPlaceholder && !element.numberPlaceholder);

		}
		else {
			$uiElementToolbar.addClass('fpd-type-image');
		}

		//ADVANCED EDITING
		if(element.advancedEditing && source &&
			(FPDUtil.isSVG(element) || $.inArray(sourceExt, allowedImageTypes) !== -1 || sourceExt.search(/data:image\/(jpeg|png);/) !== -1)) {
			_toggleNavItem('advanced-editing');
		}

		_toggleNavItem('reset');
		_toggleNavItem('duplicate', element.copyable);
		_toggleNavItem('remove', element.removable);

		//display only enabled tabs and when tabs length > 1
		$uiToolbarSub.find('.fpd-panel-tabs').each(function(index, panelTabs) {

			var $panelTabs = $(panelTabs);
			$panelTabs.toggleClass('fpd-hidden', $panelTabs.children(':not(.fpd-disabled)').length <= 1);
			$panelTabs.children(':not(.fpd-disabled):first').addClass('fpd-active').click();

		});

		//set UI value by selected element
		$uiElementToolbar.find('[data-control]').each(function(index, uiElement) {

			var $uiElement = $(uiElement),
				parameter = $uiElement.data('control');

			if($uiElement.hasClass('fpd-number')) {

				if(element[parameter] !== undefined) {

					var numVal = $uiElement.attr('step') && $uiElement.attr('step').length > 1 ? parseFloat(element[parameter]).toFixed(2) : parseInt(element[parameter]);
					$uiElement.val(numVal);

					if($uiElement.prev('.fpd-range-wrapper')) {

						if(parameter == 'fontSize') {
							$uiElement.prev('.fpd-range-wrapper').children('input')
							.attr('min', element.minFontSize)
							.attr('max', element.maxFontSize);
						}
						else if(parameter == 'scaleX' || parameter == 'scaleY') {
							$uiElement.prev('.fpd-range-wrapper').children('input')
							.attr('min', element.minScaleLimit)
						}

						$uiElement.prev('.fpd-range-wrapper').children('input').val(numVal)
						.rangeslider('update', true, false);

					}

				}

			}
			else if($uiElement.hasClass('fpd-toggle')) {

				$uiElement.toggleClass('fpd-enabled', element[parameter] === $uiElement.data('enabled'));

			}
			else if($uiElement.hasClass('fpd-btn-options')) {

				$uiElement.children('span:first').removeClass()
				.addClass($uiElement.data('options')[element[parameter]]);

			}
			else if(parameter == 'fontSize') {
				$uiElementToolbar.find('.fpd-tool-text-size > .fpd-current-val').text(parseInt(element[parameter]));
			}
			else if(parameter == 'fontFamily') {

				if(element[parameter] !== undefined) {

					$uiElementToolbar
					.find('.fpd-fonts-list .fpd-item').removeClass('fpd-active')
					.filter('[data-value="'+element[parameter]+'"]').addClass('fpd-active')

				}

			}

			var currentFill = FPDUtil.getBgCssFromElement(element);
			if(currentFill) {

				$uiElementToolbar.find('.fpd-current-fill').css('background', currentFill);

			}


		});

		//select first visible nav item
		if(fpdInstance.$container.hasClass('fpd-device-desktop')) {
			$uiElementToolbar.find('.fpd-tools-nav > [data-panel]:not(.fpd-hidden):first').click();
		}
		else {
			instance.toggle(true, false)
		}

		$uiElementToolbar.children('.fpd-scroll-area').scrollTop(0);
		$uiElementToolbar.children('.fpd-scroll-area').scrollLeft(0);


	};

	this.updateUIValue = function(tool, value) {

		var $UIController = $uiElementToolbar.find('[data-control="'+tool+'"]');

		$UIController.val(value);
		$UIController.filter('.fpd-slider-range').rangeslider('update', true, false);

	};

	this.hideTools = function() {

		$uiElementToolbar
		.removeClass('fpd-panel-visible')
		.find('.fpd-tools-nav > div')
		.addClass('fpd-hidden').removeClass('fpd-active'); //hide tool in row

		$uiToolbarSub
		.children().removeClass('fpd-active')//hide all sub panels in sub toolbar
		.find('.fpd-panel-tabs > span').addClass('fpd-disabled').removeClass('fpd-hidden') //disable all tabs

		//remove active tabs
		$uiToolbarSub.find('.fpd-panel-tabs-content, .fpd-panel-tabs').children().removeClass('fpd-active')

		$colorPicker.addClass('fpd-hidden');

	};

	this.updatePosition = function(element, showHide) {

	};

	this.toggle = function(showHide, reset) {

		showHide = showHide === undefined ? true : showHide;
		reset = reset === undefined ? true : reset;

		if(!showHide && reset) {
			$colorPicker.spectrum('destroy');
		}

		$uiElementToolbar.toggleClass('fpd-show', showHide);

	};

	this.setPlacement = function(placement) {

		instance.placement = placement;

		if(placement == 'body') {
			$uiElementToolbar.appendTo(fpdInstance.mainOptions.toolbarDynamicContext);
		}
		else {
			$uiElementToolbar.appendTo(fpdInstance.mainBar.$container);
		}

	};

	_initialize();

};

/**
 * This is the main entry point to access FPD via the API. FancyProductDesigner class contains the instances of {{#crossLink "FancyProductDesignerView"}}FancyProductDesignerView{{/crossLink}} class.
 *
 *<h5>Example</h5>
 * Best practice to use the API is to wait for the ready event, then the UI and all products/designs has been set (not loaded).<pre>var fpd = new FancyProductDesigner($fpd, options);
 $fpd.on('ready', function() { //use api methods in here })</pre>
 * @class FancyProductDesigner
 * @constructor
 * @param {HTMLElement | jQuery} elem - A HTML element with an unique ID.
 * @param {Object} [opts] - See {{#crossLink "Options.defaults"}}{{/crossLink}}.
 */
var FancyProductDesigner = function(elem, opts) {

	'use strict';

	$ = jQuery;

	var instance = this,
		$window = $(window),
		$body = $('body'),
		$products,
		$designs,
		$elem,
		$mainBar,
		$stageLoader,
		$uiElements,
		$modules,
		$editorBox = null,
		$thumbnailPreview = null,
		stageCleared = false,
		zoomReseted = false,
		firstProductCreated = false,
		inTextField = false,
		initCSSClasses = '',
		$draggedImage,
		_totalProductElements = 0,
		_productElementLoadingIndex = 0,
		_outOfBoundingBoxLabel = '';

	/**
	 * Array containing all added products categorized.
	 *
	 * @property products
	 * @type Array
	 */
	this.products = [];

	/**
	 * Array containing all added designs.
	 *
	 * @property designs
	 * @type Array
	 */
	this.designs = [];

	/**
	 * The current selected product category index.
	 *
	 * @property currentCategoryIndex
	 * @type Number
	 * @default 0
	 */
	this.currentCategoryIndex = 0;

	/**
	 * The current selected product index.
	 *
	 * @property currentProductIndex
	 * @type Number
	 * @default 0
	 */
	this.currentProductIndex = 0;

	/**
	 * The current selected view index.
	 *
	 * @property currentViewIndex
	 * @type Number
	 * @default 0
	 */
	this.currentViewIndex = 0;

	/**
	 * The price considering the elements price in all views with order quantity.
	 *
	 * @property currentPrice
	 * @type Number
	 * @default 0
	 */
	this.currentPrice = 0;

	/**
	 * The price considering the elements price in all views without order quantity.
	 *
	 * @property singleProductPrice
	 * @type Number
	 * @default 0
	 */
	this.singleProductPrice = 0;

	/**
	 * The current views.
	 *
	 * @property currentViews
	 * @type Array
	 * @default null
	 */
	this.currentViews = null;

	/**
	 * The current view instance.
	 *
	 * @property currentViewInstance
	 * @type FancyProductDesignerView
	 * @default null
	 */
	this.currentViewInstance = null;

	/**
	 * The current selected element.
	 *
	 * @property currentElement
	 * @type fabric.Object
	 * @default null
	 */
	this.currentElement = null;

	/**
	 * JSON Object containing all translations.
	 *
	 * @property langJson
	 * @type Object
	 * @default null
	 */
	this.langJson = null;

	/**
	 * The main options set for this Product Designer.
	 *
	 * @property mainOptions
	 * @type Object
	 */
	this.mainOptions;

	/**
	 * jQuery object pointing on the product stage.
	 *
	 * @property $productStage
	 * @type jQuery
	 */
	this.$productStage = null;

	/**
	 * jQuery object pointing on the tooltip for the current selected element.
	 *
	 * @property $elementTooltip
	 * @type jQuery
	 */
	this.$elementTooltip = null;

	/**
	 * URL to the watermark image if one is set via options.
	 *
	 * @property watermarkImg
	 * @type String
	 * @default null
	 */
	this.watermarkImg = null;

	/**
	 * Indicates if the product is created or not.
	 *
	 * @property productCreated
	 * @type Boolean
	 * @default false
	 */
	this.productCreated = false;

	/**
	 * Indicates if the product was saved.
	 *
	 * @property doUnsavedAlert
	 * @type Boolean
	 * @default false
	 */
	this.doUnsavedAlert = false;

	/**
	 * Array containing all FancyProductDesignerView instances of the current showing product.
	 *
	 * @property viewInstances
	 * @type Array
	 * @default []
	 */
	this.viewInstances = [];

	/**
	 * Object containing all color link groups.
	 *
	 * @property colorLinkGroups
	 * @type Object
	 * @default {}
	 */
	this.colorLinkGroups = {};

	/**
	 * The order quantity.
	 *
	 * @property orderQuantity
	 * @type Number
	 * @default 1
	 */
	this.orderQuantity = 1;

	/**
	 * If FPDBulkVariations is used with the product designer, this is the instance to the FPDBulkVariations class.
	 *
	 * @property bulkVariations
	 * @type FPDBulkVariations
	 * @default null
	 */
	this.bulkVariations = null;

	/**
	 * The calculated price for the pricing rules.
	 *
	 * @property pricingRulesPrice
	 * @type Number
	 * @default 0
	 */
	this.pricingRulesPrice = 0;

	/**
	 * The container for internal modals.
	 *
	 * @property $modalContainer
	 * @type jQuery
	 * @default 0
	 */
	this.$modalContainer = $('body');

	/**
	 * Array will all added custom elements.
	 *
	 * @property globalCustomElements
	 * @type Array
	 * @default []
	 */
	this.globalCustomElements = [];

	/**
	 * Array will all fixed elements.
	 *
	 * @property fixedElements
	 * @type Array
	 * @default []
	 */
	this.fixedElements = [];

	/**
	 * Returns if mouse is over a fabricJS canvas and in which case the fabricJS object.
	 *
	 * @property mouseOverCanvas
	 * @type Boolean
	 * @default false
	 */
	this.mouseOverCanvas = false;

	this.languageJSON = {
		"toolbar": {},
		"actions": {},
		"modules": {},
		"misc": {},
		"image_editor": {},
		"plus": {}
	};
	this._order = {};
	this._loadingCustomImage = false;
	this._prevPrintingBoxes = [];

	var fpdOptionsInstance = new FancyProductDesignerOptions();
	this.mainOptions = fpdOptionsInstance.merge(fpdOptionsInstance.defaults, opts);


	var _initialize = function() {

		// @@include('../envato/evilDomain.js')

		//create custom jquery expression to ignore case when filtering
		$.expr[":"].containsCaseInsensitive = $.expr.createPseudo(function(arg) {
		    return function( elem ) {
		        return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
		    };
		});

		//check if element is a jquery object
		if(elem instanceof jQuery) {
			$elem = elem;
		}
		else {
			$elem = $(elem);
		}

		instance.$modalContainer = instance.mainOptions.openModalInDesigner ? $elem : $('body');

		$elem.removeClass('fpd-module-visible fpd-ui-theme-doyle');

		initCSSClasses = $elem.attr('class') ? $elem.attr('class') : '';

		instance.mainOptions.mainBarContainer = instance.mainOptions.modalMode !== false ? false : instance.mainOptions.mainBarContainer;

		//force sidebar when main bar container is set
		if(instance.mainOptions.mainBarContainer) {
			$elem.removeClass('fpd-sidebar').addClass('fpd-topbar');
		}

		if(!instance.mainOptions.fabricCanvasOptions.allowTouchScrolling) {
			$elem.addClass('fpd-disable-touch-scrolling');
		}

		//doyle setup
		$elem.addClass('fpd-device-'+FPDUtil.getDeviceByScreenSize());
		$elem.addClass('fpd-ui-theme-'+instance.mainOptions.uiTheme);
		if(instance.mainOptions.uiTheme == 'doyle') {
			$elem.removeClass('fpd-topbar fpd-tabs-top').addClass('fpd-sidebar fpd-tabs-side')
		}

		instance.$container = $elem.data('instance', instance);

		//save products and designs HTML
		$products = $elem.children('.fpd-category').length > 0 ? $elem.children('.fpd-category').remove() : $elem.children('.fpd-product').remove();
		$designs = $elem.find('.fpd-design > .fpd-category').length > 0 ? $elem.find('.fpd-design > .fpd-category') : $elem.find('.fpd-design > img');
		$elem.children('.fpd-design').remove();

		//add product designer into modal
		if(instance.mainOptions.modalMode) {

			$elem.removeClass('fpd-hidden');
			$body.addClass('fpd-modal-mode-active');

			var $modalProductDesigner = $elem.wrap('<div class="fpd-modal-product-designer fpd-modal-overlay fpd-fullscreen"><div class="fpd-modal-wrapper"></div></div>').parents('.fpd-modal-overlay:first'),
				modalProductDesignerOnceOpened = false;

			$modalProductDesigner.children()
			.append('<div class="fpd-done fpd-btn" data-defaulttext="Done">misc.modal_done</div><div class="fpd-modal-close"><span class="fpd-icon-close"></span></div>');

			$modalProductDesigner.addClass('fpd-ui-theme-'+instance.mainOptions.uiTheme)

			$(instance.mainOptions.modalMode).addClass('fpd-modal-mode-btn').click(function(evt) {

				evt.preventDefault();

				$body.addClass('fpd-overflow-hidden').removeClass('fpd-modal-mode-active');
				$modalProductDesigner.addClass('fpd-fullscreen').fadeIn(300);

				if(instance.currentViewInstance) {
					instance.currentViewInstance.resetCanvasSize();
					instance.resetZoom();
				}

				var $selectedModule = $mainBar.children('.fpd-navigation').children('.fpd-active');
				if($selectedModule.length > 0) {
					instance.mainBar.callModule($selectedModule.data('module'));
				}

				//auto-select
				var autoSelectElement = null;
				if(!modalProductDesignerOnceOpened) {

					if(!instance.mainOptions.editorMode && instance.currentViewInstance) {

						var viewElements = instance.currentViewInstance.stage.getObjects();
						for(var i=0; i < viewElements.length; ++i) {
							var obj = viewElements[i];

							 if(obj.autoSelect && !obj.hasUploadZone) {
								 autoSelectElement = obj;
							 }

						}

					}

				}

				setTimeout(function() {

					if(autoSelectElement) {
						instance.currentViewInstance.stage.setActiveObject(autoSelectElement);
						instance.currentViewInstance.stage.renderAll();
					}

				}, 300);

				modalProductDesignerOnceOpened = true;

				/**
			     * Gets fired when the modal with the product designer opens.
			     *
			     * @event FancyProductDesigner#modalDesignerOpen
			     * @param {Event} event
			     */
				instance.$container.trigger('modalDesignerOpen');

			});

			$modalProductDesigner.find('.fpd-done').click(function() {

				$modalProductDesigner.find('.fpd-modal-close').click();

				/**
			     * Gets fired when the modal with the product designer closes.
			     *
			     * @event FancyProductDesigner#modalDesignerClose
			     * @param {Event} event
			     */
				instance.$container.trigger('modalDesignerClose');

			});

		}

		//test if browser is supported (safari, chrome, opera, firefox IE>9)
		var canvasTest = document.createElement('canvas'),
			canvasIsSupported = Boolean(canvasTest.getContext && canvasTest.getContext('2d')),
			minIE = instance.mainOptions.templatesDirectory ? 9 : 8;

		if(!canvasIsSupported || (FPDUtil.isIE() && Number(FPDUtil.isIE()) <= minIE)) {

			_loadTemplate('canvaserror', instance.mainOptions.templatesType, 0, function(html) {

				$elem.append($.parseHTML(html)).fadeIn(300);
				$elem.trigger('templateLoad', [this.url]);

			});

			/**
		     * Gets fired when the browser does not support HTML5 canvas.
		     *
		     * @event FancyProductDesigner#canvasFail
		     * @param {Event} event
		     */
			$elem.trigger('canvasFail');

			return false;
		}

		//lowercase all keys in hexNames
		var key,
			keys = Object.keys(instance.mainOptions.hexNames),
			n = keys.length,
			newHexNames = {};

		Object.keys(instance.mainOptions.hexNames).forEach(function(hexKey) {
			newHexNames[hexKey.toLowerCase()] = instance.mainOptions.hexNames[hexKey];
		});
		instance.mainOptions.hexNames = newHexNames;

		//sort fonts
		if(instance.mainOptions.fonts && instance.mainOptions.fonts.length > 0) {

			//fonts array has objects
			if(typeof instance.mainOptions.fonts[0] === 'object') {
				instance.mainOptions.fonts.sort(function(a, b) {
					var nameA = a.name.toUpperCase(), // ignore upper and lowercase
						nameB = b.name.toUpperCase(); // ignore upper and lowercase
					if (nameA < nameB) {
						return -1;
					}
					if (nameA > nameB) {
						return 1;
					}

					//same
					return 0;
				});
			}
			else {
				instance.mainOptions.fonts.sort();
			}

		}

		//PLUS
		if(typeof FancyProductDesignerPlus !== 'undefined') {
			FancyProductDesignerPlus.setup($elem, instance);
		}

		//PRICING RULES
		if(typeof FPDPricingRules !== 'undefined') {
			instance.pricingRulesInstance = new FPDPricingRules($elem, instance);
		}

		//load language JSON
		if(instance.mainOptions.langJSON !== false) {

			if(typeof instance.mainOptions.langJSON === 'object') {

				instance.langJson = instance.mainOptions.langJSON;

				$elem.trigger('langJSONLoad', [instance.langJson]);

				_initProductStage();

			}
			else {

				$.getJSON(instance.mainOptions.langJSON).done(function(data) {

					instance.langJson = data;

					/**
				     * Gets fired when the language JSON is loaded.
				     *
				     * @event FancyProductDesigner#langJSONLoad
				     * @param {Event} event
				     * @param {Object} langJSON - A JSON containing the translation.
				     */
					$elem.trigger('langJSONLoad', [instance.langJson]);

					_initProductStage();

				})
				.fail(function(data) {

					FPDUtil.showModal('Language JSON "'+instance.mainOptions.langJSON+'" could not be loaded or is not valid. Make sure you set the correct URL in the options and the JSON is valid!');

					$elem.trigger('langJSONLoad', [instance.langJson]);
				});

			}


		}
		else {
			_initProductStage();
		}

	}; //init end

	//init the product stage
	var _initProductStage = function() {

		var loaderHTML = '<div class="fpd-loader-wrapper"><div class="fpd-loader"><div class="fpd-loader-circle"></div><span class="fpd-loader-text" data-defaulttext="Initializing Product Designer">misc.initializing</span></div></div>',
			tooltipHtml = '<div class="fpd-element-tooltip" style="display: none;" data-defaulttext="Move element in its containment!">misc.out_of_bounding_box</div>';

		//add init loader
		instance.$mainWrapper = $elem.addClass('fpd-container fpd-clearfix fpd-grid-columns-'+instance.mainOptions.gridColumns).html(loaderHTML+'<div class="fpd-main-wrapper">'+tooltipHtml+'<div class="fpd-snap-line-h"></div><div class="fpd-snap-line-v"></div><div class="fpd-product-stage" style="width:'+instance.mainOptions.stageWidth+'px;height: '+instance.mainOptions.stageHeight+'px;"></div></div>').children('.fpd-main-wrapper');

		instance.$actionsWrapper = $('<div class="fpd-actions-container"></div>');
		if(instance.mainOptions.uiTheme == 'doyle') {
			instance.$actionsWrapper.addClass('fpd-primary-bg-color').prependTo(instance.$container);
		}
		else { //flat
			instance.$actionsWrapper.appendTo(instance.$mainWrapper);
		}


		if(!instance.mainOptions.editorMode) {
			$elem.after('<div class="fpd-device-info">'+instance.getTranslation('misc', 'not_supported_device_info')+'</div>');
		}

		instance.$mainWrapper.prepend('<div class="fpd-modal-lock"><div class="fpd-toggle-lock"><span class="fpd-icon-unlocked"></span><span class="fpd-icon-locked"></span><div>'+instance.getTranslation('misc', 'view_optional_unlock')+'</div></div></div>');

		instance.$productStage  = instance.$mainWrapper.children('.fpd-product-stage')
		instance.$elementTooltip = instance.$mainWrapper.children('.fpd-element-tooltip');
		$stageLoader = $elem.children('.fpd-loader-wrapper');

		instance.translateElement($stageLoader.find('.fpd-loader-text'));
		_outOfBoundingBoxLabel = instance.translateElement(instance.$elementTooltip);
		if(instance.mainOptions.modalMode) {
			instance.translateElement($body.find('.fpd-modal-overlay .fpd-done'));
		}

		//load editor box if requested
		if(typeof instance.mainOptions.editorMode === 'string') {

			$editorBox = $('<div class="fpd-editor-box"><h5></h5><div class="fpd-clearfix"></div></div>');
			$(instance.mainOptions.editorMode).append($editorBox);

		}

		$body.on('focus blur', '[class^="fpd-element-toolbar"] textarea, [class^="fpd-element-toolbar"] input[type="number"], [class^="fpd-element-toolbar"] input[type="text"]', function(evt) {
			inTextField = evt.type == 'focusin';

		});

		instance.$container.on('screenSizeChange', function(evt, device){

			if(instance.mainOptions.uiTheme !== 'doyle') {

				if(device === 'smartphone') {
					instance.$actionsWrapper.insertBefore(instance.$mainWrapper);
				}
				else {
					instance.$actionsWrapper.appendTo(instance.$mainWrapper);
				}

			}


		});

		//window resize handler
		var device = '';
		$window.resize(function() {

			//fix for android browser, because keyboard trigger resize event
			if(inTextField) {
				return;
			}

			if(instance.currentViewInstance) {
				instance.currentViewInstance.resetCanvasSize();
			}

			if(instance.mainBar && instance.mainBar.$content
				&& instance.$container.filter('[class*="fpd-off-canvas-"]').length > 0) {
				instance.mainBar.$content.height(instance.$mainWrapper.height());
			}

			if(instance.actions) {

				instance.actions.hideAllTooltips();
				if(!zoomReseted) {
					instance.resetZoom();
				}

			}

			//deselect element if one is selected and active element is not input (FB browser fix)
			//alert(document.activeElement);
			if(instance.currentElement && $(document.activeElement).is(':not(input)') && $(document.activeElement).is(':not(textarea)')) {
				instance.deselectElement();
			}

			if((instance.currentElement && instance.currentElement.isEditing) || instance.mainOptions.editorMode) {
				return;
			}

			var currentDevice = FPDUtil.getDeviceByScreenSize();
			if(currentDevice == 'smartphone') {

				if(!instance.$container.hasClass('fpd-topbar') && instance.mainBar) {

					if(instance.mainOptions.uiTheme !== 'doyle') {

						instance.$container.removeClass('fpd-sidebar').addClass('fpd-topbar');
						instance.mainBar.setContentWrapper('draggable-dialog');

					}

				}

			}
			else if(currentDevice == 'tablet') {
			}
			else if(currentDevice == 'desktop') {

				if(instance.mainOptions.uiTheme !== 'doyle' && initCSSClasses.search('fpd-topbar') === -1 && instance.$container.hasClass('fpd-topbar')) {

					instance.$container.removeClass('fpd-topbar').addClass('fpd-sidebar');

					if(instance.mainBar && !instance.mainOptions.mainBarContainer) {
						instance.mainBar.setContentWrapper('sidebar');
					}
				}

			}

			if(device !== currentDevice) {

				/**
			     * Gets fired as soon as the screen size has changed. Breakpoints: Smartphone Width < 568, Tablet Width > 568 and < 768, Desktop Width > 768.
			     *
			     * @event FancyProductDesigner#canvasFail
			     * @param {Event} event
			     * @param {String} device Possible values: desktop, tablet, smartphone.
			     */
				$elem.trigger('screenSizeChange', [currentDevice]);
			}

			if(instance.currentViewInstance) {
				instance.currentViewInstance.resetCanvasSize();
			}

			device = currentDevice;

		});

		instance.loadFonts(instance.mainOptions.fonts, function() {
			instance.mainOptions.templatesDirectory ?
				_loadTemplate('productdesigner', instance.mainOptions.templatesType, 0, _loadProductDesignerTemplate)
			:
				_ready();
		});

	};

	//now load UI elements from external HTML file
	var _loadProductDesignerTemplate = function(html) {

		/**
	     * Gets fired as soon as a template has been loaded.
	     *
	     * @event FancyProductDesigner#templateLoad
	     * @param {Event} event
	     * @param {string} URL - The URL of the loaded template.
	     */
		$elem.trigger('templateLoad', [this.url]);

		$uiElements = $(html);

		$uiElements.find('[data-defaulttext]').each(function(index, uiElement) {

			instance.translateElement($(uiElement));

		});

		instance.translatedUI = $uiElements;

		if(instance.mainOptions.mainBarContainer) {

			$elem.addClass('fpd-main-bar-container-enabled');
			$mainBar = $(instance.mainOptions.mainBarContainer).addClass('fpd-container fpd-main-bar-container fpd-tabs fpd-tabs-top fpd-sidebar fpd-grid-columns-'+instance.mainOptions.gridColumns).html($uiElements.children('.fpd-mainbar')).children('.fpd-mainbar');

		}
		else {
			$mainBar = $uiElements.children('.fpd-mainbar').insertBefore($elem.children('.fpd-loader-wrapper'));
		}

		$modules = $uiElements.children('.fpd-modules');

		if($elem.hasClass('fpd-sidebar')) {
			$elem.height(instance.mainOptions.stageHeight);
		}
		else {
			$elem.width(instance.mainOptions.stageWidth);
		}

		//show tabs content
		$body.on('click', '.fpd-module-tabs > div', function() {

			var $this = $(this),
				context = $(this).data('context');

			$this.addClass('fpd-active').siblings().removeClass('fpd-active');
			$this.parent().next('.fpd-module-tabs-content').children().hide().filter('[data-context="'+context+'"]').show();

		});

		//setup modules
		if(instance.mainOptions.mainBarModules) {

			instance.mainBar = new FPDMainBar(
				instance,
				$mainBar,
				$modules,
				$uiElements.children('.fpd-draggable-dialog')
			);

		}

		//init Actions
		if(instance.mainOptions.actions) {
			instance.actions = new FPDActions(instance, $uiElements.children('.fpd-actions'));
		}

		/**
	     * Gets fired as soon as the user interface with all modules, actions is set and translated.
	     *
	     * @event FancyProductDesigner#uiSet
	     * @param {Event} event
	     */
		$elem.trigger('uiSet');

		//init Toolbar
		var $elementToolbar = $uiElements.children('.fpd-element-toolbar');
		if(instance.mainOptions.uiTheme === 'doyle') {
			$elementToolbar = $uiElements.children('.fpd-element-toolbar-side');
			instance.toolbar = new FPDToolbarSide($elementToolbar, instance);
		}
		else {
			$elementToolbar = $uiElements.children('.fpd-element-toolbar-smart');
			instance.toolbar = new FPDToolbarSmart($elementToolbar, instance);
		}

		var zoomStart = 1,
			zoomDiff = undefined,
			zoomEnd = 1;

		$elem.on('elementSelect', function(evt, element) {

			evt.stopPropagation();
			evt.preventDefault();

			if(element && !element._ignore && instance.currentViewInstance) {

				//upload zone is selected
				if(element.uploadZone && !instance.mainOptions.editorMode) {

					element.set('borderColor', 'transparent');

					var customAdds = $.extend(
						{},
						instance.currentViewInstance.options.customAdds,
						element.customAdds ? element.customAdds : {}
					);

					//mobile fix: elementSelect is triggered before click, this was adding an image on mobile
					setTimeout(function() {

						instance.currentViewInstance.currentUploadZone = element.title;
						instance.mainBar.toggleUploadZoneAdds(customAdds);
						instance.mainBar.toggleUploadZonePanel();

					}, 100);


					return;
				}
				//if element has no upload zone and an upload zone is selected, close dialogs and call first module
				else if(instance.currentViewInstance.currentUploadZone) {

					instance.mainBar.toggleDialog(false);
					instance.mainBar.toggleUploadZonePanel(false);


				}

				instance.toolbar.update(element);

				if(instance.mainOptions.openTextInputOnSelect && FPDUtil.getType(element.type) === 'text' && element.editable) {

					$elementToolbar.find('.fpd-tool-edit-text:first').click();
				}

				_updateEditorBox(element);

			}
			else {

				instance.toolbar.toggle(false);
				$body.children('[class^="fpd-element-toolbar"]').find('input').spectrum('destroy');

			}

		})
		.on('elementChange', function(evt, type, element) {

			if(!element._ignore && instance.mainOptions.uiTheme !== 'doyle') {
				instance.toolbar.toggle(false, false);
			}

			if(instance.mainOptions.fabricCanvasOptions.allowTouchScrolling) {
				$elem.addClass('fpd-disable-touch-scrolling');
				instance.currentViewInstance.stage.allowTouchScrolling = false;
			}

		})
		.on('elementModify', function(evt, element, parameters) {

			if(instance.productCreated && !element._ignore) {

				if(!instance.toolbar.isTransforming) {

					if(parameters.fontSize !== undefined) {
						instance.toolbar.updateUIValue('fontSize', Number(parameters.fontSize));
					}

					if(parameters.scaleX !== undefined) {
						instance.toolbar.updateUIValue('scaleX', parseFloat(Number(parameters.scaleX).toFixed(2)));
					}

					if(parameters.scaleY !== undefined) {
						instance.toolbar.updateUIValue('scaleY', parseFloat(Number(parameters.scaleY).toFixed(2)));
					}

					if(parameters.angle !== undefined) {
						instance.toolbar.updateUIValue('angle', parseInt(parameters.angle));
					}

					if(parameters.text !== undefined) {
						instance.toolbar.updateUIValue('text', parameters.text);
					}

					if(instance.currentElement && !instance.currentElement.uploadZone) {
						instance.toolbar.updatePosition(instance.currentElement);
					}

					if(parameters.shadowColor !== undefined && parameters.shadowColor == '') {

						instance.toolbar.updateUIValue('shadowBlur', 0);
						instance.toolbar.updateUIValue('shadowOffsetX', 0);
						instance.toolbar.updateUIValue('shadowOffsetY', 0);

					}

				}

				//text link group
				if(parameters.text && !FPDUtil.isEmpty(element.textLinkGroup)) {

					for(var i=0; i < instance.viewInstances.length; ++i) {

						instance.viewInstances[i].fCanv.getObjects().forEach(function(obj) {

							if(obj !== element && FPDUtil.getType(obj.type) === 'text' && obj.textLinkGroup === element.textLinkGroup) {
								obj.set('text', element.text);
								$elem.trigger('_doPricingRules');
							}

						})

					}

				}

				if(!FPDUtil.isEmpty(element.textLinkGroup)) {
					var textLinkGroupProps = instance.mainOptions.textLinkGroupProps || [];
					Object.keys(parameters).forEach(function(param) {

						if(textLinkGroupProps.indexOf(param) != -1) {

							instance.viewInstances.forEach(function(viewInstance) {

								viewInstance.fCanv.getObjects().forEach(function(obj) {

									if(obj !== element && FPDUtil.getType(obj.type) === 'text' && obj.textLinkGroup === element.textLinkGroup) {

										var value = element[param];

										if(param == 'textDecoration') {
											obj.set('underline', value === 'underline');
										}

										if(param == 'letterSpacing') {
											obj.set('charSpacing', value * 100);
										}

										obj.set(param, value);

										$elem.trigger('_doPricingRules');


									}

								})

							})

						}


					});
				}

			}

		})
		.on('screenSizeChange', function(evt, device) {

			$elem.removeClass('fpd-device-smartphone fpd-device-tablet fpd-device-desktop')
			.addClass('fpd-device-'+device);

		})
/*
		.on('touchend', '.fpd-view-stage', function(evt) {

			evt.preventDefault();

			if(zoomDiff !== undefined) {
				zoomStart = zoomEnd;
			}

		})
		.on('touchmove', '.fpd-view-stage', function(evt) {

			evt.preventDefault();

			if(evt.originalEvent.touches.length == 2) {

				var hypo1 = Math.hypot((event.targetTouches[0].pageX - event.targetTouches[1].pageX),
	                (event.targetTouches[0].pageY - event.targetTouches[1].pageY));

	            if (zoomDiff === undefined) {
	                zoomDiff = hypo1;
	            }

	            zoomEnd = hypo1/zoomDiff;

	            instance.setZoom(zoomStart + zoomEnd);

			}

		})
*/

		//switchers
		$('.fpd-switch-container').click(function() {

			var $this = $(this);

			if($this.hasClass('fpd-curved-text-switcher')) {

				var z = instance.currentViewInstance.getZIndex(instance.currentElement),
					defaultText = instance.currentElement.get('text'),
					parameters = instance.currentViewInstance.getElementJSON(instance.currentElement);

				parameters.z = z;
				parameters.curved = instance.currentElement.type == 'i-text';
				parameters.textAlign = 'center';

				delete parameters['shadow'];

				function _onTextModeChanged(evt, textElement) {
					instance.currentViewInstance.stage.setActiveObject(textElement);
					$elem.off('elementAdd', _onTextModeChanged);

					setTimeout(function() {
						$('.fpd-tool-curved-text').click();
					}, 100);

				};
				$elem.on('elementAdd', _onTextModeChanged);

				instance.currentViewInstance.removeElement(instance.currentElement);
				instance.currentViewInstance.addElement('text', defaultText, defaultText, parameters);

			}

		});

		$('.fpd-dropdown').click(function() {

			$(this).toggleClass('fpd-active');

		});

		$body.on('click', '.fpd-views-wrapper .fpd-view-prev, .fpd-views-wrapper .fpd-view-next', function() {

			if($(this).hasClass('fpd-view-prev')) {
				instance.selectView(instance.currentViewIndex - 1);
			}
			else {
				instance.selectView(instance.currentViewIndex + 1);
			}

		})

		//drag image items on canvas or upload zone
		var itemDragged = false,
			$targetDraggedItem;

		$body
		.on('mousedown touchdown', '.fpd-grid .fpd-item:not(.fpd-category):not(.fpd-loading)', function(evt) {

			if(instance.mainOptions.dragDropImagesToUploadZones && evt.which == 1) { //only left mouse button

				$targetDraggedItem = $(this);

				itemDragged = false;
				$draggedImage = $('<div class="fpd-dragged-image fpd-hidden"><picture></picture></div>');
				FPDUtil.loadGridImage(
					$draggedImage.children('picture'),
					$targetDraggedItem.data('thumbnail') ? $targetDraggedItem.data('thumbnail'): $targetDraggedItem.data('source')
				);

				$body.append($draggedImage);
				$('.fpd-thumbnail-preview').remove();

			}

		})
		.on('mousemove', function(evt) {

			itemDragged = true;

			if($draggedImage) {

				var leftPos = evt.pageX + 10 + $draggedImage.outerWidth() > $window.width() ? $window.width() - $draggedImage.outerWidth() : evt.pageX + 10;
				$draggedImage.css({left: evt.pageX - ($draggedImage.width() * 0.5), top: evt.pageY - ($draggedImage.height() * 0.5)});

				$body.children('.fpd-dragged-image').removeClass('fpd-hidden');
				setTimeout(function() {

					if($draggedImage) {
						$draggedImage.addClass('fpd-animate');
					}


				}, 1);

				evt.stopPropagation();
				evt.preventDefault();

			}

		})
		.on('mouseup', function(evt) {

			if(!instance._loadingCustomImage && itemDragged && $draggedImage && instance.mouseOverCanvas) {

				instance._addGridItemToStage(
					$targetDraggedItem,
					instance.mouseOverCanvas.uploadZone ? {_addToUZ: instance.mouseOverCanvas.title} : {}
				);

			}

			$body.children('.fpd-dragged-image').remove();
			$draggedImage = null;

		});

		_ready();

	};

	var _ready = function() {

		//load watermark image
		if(instance.mainOptions.watermark && instance.mainOptions.watermark.length > 3) {

			fabric.Image.fromURL(instance.mainOptions.watermark, function(oImg) {
				instance.watermarkImg = oImg;
			}, {crossOrigin: "anonymous"});

		}

		if(instance.mainOptions.unsavedProductAlert) {

			window.onbeforeunload = function () {

				if(instance.doUnsavedAlert) {
					return '';
				}

			};

		}


		//window.localStorage.setItem('fpd-gt-closed', 'no');

		//store a boolean to detect if the text in textarea (toolbar) was selected, then dont deselect
		var _fixSelectionTextarea = false;

		//general close handler for modal
		$body.on('click', '.fpd-modal-close', function(evt) {

			var $this = $(this),
				$modal = $this.parents('.fpd-modal-overlay:first');

			if($this.parents('.fpd-modal-product-designer:first').length) {
				$body.addClass('fpd-modal-mode-active');
			}

			$modal.fadeOut(200, function() {

				$this.removeClass('fpd-fullscreen');

				if(!$modal.hasClass('fpd-modal-product-designer')) {
					$modal.trigger('modalRemove').remove();
				}

				$elem.trigger('modalClose');

			});

			//modal product designer is closing
			if($this.parents('.fpd-modal-product-designer:first').length > 0) {
				$body.removeClass('fpd-overflow-hidden');
				instance.deselectElement();
			}
			else if($body.find('.fpd-modal-product-designer').length == 0) {
				$body.removeClass('fpd-overflow-hidden');
			}


		})
		.on('mouseup touchend', function(evt) {

			var $target = $(evt.target);

			//deselect element if click outside of a fpd-container
			if($target.closest('.fpd-container, [class^="fpd-element-toolbar"], .sp-container').length === 0
				&& instance.mainOptions.deselectActiveOnOutside && !_fixSelectionTextarea) {

				   instance.deselectElement();

			}

			//close upload zone panel if click outside of fpd-container, needed otherwise elements can be added to upload zone e.g. mspc
			if($target.closest('.fpd-container, .fpd-modal-internal').length === 0
				&& instance.currentViewInstance && instance.currentViewInstance.currentUploadZone
				&& $stageLoader.is(':hidden')) {
				instance.mainBar.toggleUploadZonePanel(false);

			}

			_fixSelectionTextarea = false;

		})
		//thumbnail preview effect
		.on('mouseover mouseout mousemove click', '[data-module="designs"] .fpd-item, [data-module="images"] .fpd-item, [data-module="products"] .fpd-item', function(evt) {

			var $this = $(this),
				price = null;

			if(instance.currentViewInstance && instance.currentViewInstance.currentUploadZone
				&& $(evt.target).parents('.fpd-upload-zone-adds-panel').length > 0) {

				var uploadZone = instance.currentViewInstance.getUploadZone(instance.currentViewInstance.currentUploadZone);
				if(uploadZone && uploadZone.price) {
					price = uploadZone.price;
				}

			}

			if($draggedImage) { return;}

			if(evt.type === 'mouseover' && $this.data('source')) {

				//do not show when scrolling
				if($this.parents('.mCustomScrollBox:first').next('.mCSB_scrollTools_onDrag').length) {
					return;
				}

				$thumbnailPreview = $('<div class="fpd-thumbnail-preview"><picture></picture></div>');
				FPDUtil.loadGridImage($thumbnailPreview.children('picture'), $this.children('picture').data('img'));

				//thumbnails in images module
				if($this.parents('[data-module="images"]:first').length > 0 && price === null) {

					if(!isNaN($this.data('price'))) {
						price = $this.data('price');
					}
					else if(instance.currentViewInstance && instance.currentViewInstance.options.customImageParameters.price) {
						price = instance.currentViewInstance.options.customImageParameters.price;
					}

				}
				//thumbnails in designs/products module
				else {

					if($this.data('title')) {
						$thumbnailPreview.addClass('fpd-title-enabled');
						$thumbnailPreview.append('<div class="fpd-preview-title">'+$this.data('title')+'</div>');
					}

					if($this.data('parameters') && $this.data('parameters').price && price === null) {
						price = $this.data('parameters').price;
					}

				}

				if(price) {
					$thumbnailPreview.append('<div class="fpd-preview-price">'+instance.formatPrice(price)+'</div>');
				}

				if($this.children('.fpd-image-quality-ratings').length) {

					var $cloneRatings = $this.children('.fpd-image-quality-ratings').clone();

					$thumbnailPreview.append($cloneRatings);

					if($this.children('.fpd-image-quality-ratings').data('quality-label')) {
						$cloneRatings
						.append('<span class="fpd-image-quality-rating-label">'+$this.children('.fpd-image-quality-ratings').data('quality-label')+'</span>');
					}


				}


				$body.append($thumbnailPreview);

			}

			if($thumbnailPreview !== null) {

				if(evt.type === 'mousemove' || evt.type === 'mouseover') {

					var leftPos = evt.pageX + 10 + $thumbnailPreview.outerWidth() > $window.width() ? $window.width() - $thumbnailPreview.outerWidth() : evt.pageX + 10;
					$thumbnailPreview.css({left: leftPos, top: evt.pageY + 10});

				}
				else if(evt.type === 'mouseout' || evt.type == 'click') {

					$thumbnailPreview.siblings('.fpd-thumbnail-preview').remove();
					$thumbnailPreview.remove();

				}

			}

		}).
		on('mousedown', function(evt) {

			var $target = $(evt.target);
			_fixSelectionTextarea = $target.is('textarea') && $target.data('control') ? true : false;

		})
		//guided tour events
		.on('click', '.fpd-gt-close', function() {

			if(FPDUtil.localStorageAvailable()) {

				window.localStorage.setItem('fpd-gt-closed', 'yes');

			}

			$(this).parent('.fpd-gt-step').remove();

		})
		.on('click', '.fpd-gt-next, .fpd-gt-back', function() {

			instance.selectGuidedTourStep($(this).data('target'));

		});

		instance.$container
		.on('productCreate modalDesignerOpen layoutElementsAdded', function(evt, elements) {

			if((!firstProductCreated && !instance.mainOptions.modalMode) || (!firstProductCreated && evt.type === 'modalDesignerOpen')) {

				if(instance.mainOptions.autoOpenInfo) {
					instance.$container.find('[data-action="info"]').click();
				}

				if(instance.mainOptions.guidedTour && Object.keys(instance.mainOptions.guidedTour).length > 0) {

					var firstKey = Object.keys(instance.mainOptions.guidedTour)[0];

					if(FPDUtil.localStorageAvailable()) {
						if(window.localStorage.getItem('fpd-gt-closed') !== 'yes') {
							instance.selectGuidedTourStep(firstKey);
						}
					}
					else {
						instance.selectGuidedTourStep(firstKey);
					}

				}

			}

			if(['productCreate', 'layoutElementsAdded'].indexOf(evt.type) != -1 && (instance.globalCustomElements.length > 0 || instance.fixedElements.length > 0)) {

                var globalElements = instance.globalCustomElements.concat(instance.fixedElements),
                	customElementsCount = 0;

                function _addCustomElement(object) {

                    var viewInstance = instance.viewInstances[object.viewIndex];

                    if(viewInstance) { //add element to correct view

	                    var fpdElement = object.element;

						//replace printing box if global element has a printing box from previous product
	                    if(instance._prevPrintingBoxes[object.viewIndex]) {

		                    var prevPrintingBox = instance._prevPrintingBoxes[object.viewIndex];
		                    if(	typeof fpdElement.boundingBox === 'object'
		                    	&& FPDUtil.objectHasKeys(viewInstance.options.printingBox, ['left','top','width','height']))
		                    {
			                    if(JSON.stringify(prevPrintingBox) === JSON.stringify(fpdElement.boundingBox)) {
				                    fpdElement.boundingBox = viewInstance.options.printingBox;
			                    }

			                }

	                    }

                        viewInstance.addElement(
                            FPDUtil.getType(fpdElement.type),
                            fpdElement.source,
                            fpdElement.title,
                            viewInstance.getElementJSON(fpdElement)
                        );

                    }
                    else {
                        _customElementAdded();
                    }

                };

                function _customElementAdded() {

                    customElementsCount++;
                    if(customElementsCount < globalElements.length) {
                        _addCustomElement(globalElements[customElementsCount]);
                    }
                    else {
                        $elem.off('elementAdd', _customElementAdded);
                    }

                };

                $elem.on('elementAdd', _customElementAdded);
                _addCustomElement(globalElements[0]);

            }

			firstProductCreated = instance.mainOptions.modalMode && evt.type === 'modalDesignerOpen';

		})
		.on('viewSelect', function(evt, index, viewInstance) {

			var currentViewOptions = viewInstance.options,
				$items = $('[data-module="designs"] .fpd-item, [data-module="images"] .fpd-item');

			$('[data-module="designs"] .fpd-item, [data-module="images"] .fpd-item').each(function() {

				FPDUtil.setItemPrice($(this), instance);

			})

			instance.$viewSelectionWrapper.children('.fpd-view-prev, fpd-view-next').toggleClass('fpd-hidden', instance.viewInstances.length <= 1);
			instance.$viewSelectionWrapper.find('.fpd-view-prev').toggleClass('fpd-disabled', index === 0);
			instance.$viewSelectionWrapper.find('.fpd-view-next').toggleClass('fpd-disabled', index === instance.viewInstances.length - 1);

		})
		.on('secondaryModuleCalled', function(evt, className, $module) {

			FPDUtil.setItemPrice($module.find('.fpd-item'), instance);

		})

		//view lock handler
		instance.$mainWrapper.on('click', '.fpd-modal-lock > .fpd-toggle-lock', function() {

			$(this).parents('.fpd-modal-lock:first').toggleClass('fpd-unlocked');
			instance.currentViewInstance.toggleLock(!instance.currentViewInstance.locked);

		});

		if(instance.mainOptions.productsJSON) {

			if(typeof instance.mainOptions.productsJSON === 'object') {
				instance.setupProducts(instance.mainOptions.productsJSON);
			}
			else {

				$.getJSON(instance.mainOptions.productsJSON)
				.done(function(data) {
					//data = data[0];
					//data = [data];
					instance.setupProducts(data);
				})
				.fail(function() {
					FPDUtil.showModal('Products JSON could not be loaded. Please check that your URL is correct!<br>URL: <i>'+instance.mainOptions.productsJSON+'</i>');
				});

			}

		}
		else {
			_createProductJSONFromHTML($products);
		}

		if(instance.mainOptions.designsJSON) {

			if(typeof instance.mainOptions.designsJSON === 'object') {
				instance.setupDesigns(instance.mainOptions.designsJSON);
			}
			else {

				$.getJSON(instance.mainOptions.designsJSON)
				.done(function(data) {
					/*data = data[0];
					data = data.designs;*/
					instance.setupDesigns(data);
				})
				.fail(function() {
					FPDUtil.showModal('Designs JSON could not be loaded. Please check that your URL is correct!<br>URL: <i>'+instance.mainOptions.designsJSON+'</i>');
				});

			}

		}
		else {
			_createDesignJSONFromHTML($designs);
		}

		if(typeof Hammer !== 'undefined' && instance.mainOptions.mobileGesturesBehaviour != 'none') {

			var pinchElementScaleX,
				pinchElementScaleY;

			var mc = new Hammer.Manager($('.fpd-product-stage').get(0));
			mc.add(new Hammer.Pan({ threshold: 0, pointers: 2 }));
			mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get('pan')]);

			mc.on('pinchmove pinchstart', function(evt) {

				var currentElement = instance.currentElement;

				if(instance.mainOptions.mobileGesturesBehaviour == 'pinchImageScale' && currentElement && FPDUtil.getType(currentElement.type) == 'image' && currentElement.resizable) {

					if(evt.type == 'pinchstart') {

						pinchElementScaleX = currentElement.scaleX;
						pinchElementScaleY = currentElement.scaleY;

					}
					else {

						instance.currentViewInstance.setElementParameters({
							scaleX: pinchElementScaleX * evt.scale,
							scaleY: pinchElementScaleY * evt.scale,
						}, currentElement);

					}

				}
				else if(instance.mainOptions.mobileGesturesBehaviour == 'pinchPanCanvas') {
					instance.setZoom(evt.scale);
				}

			});

			mc.on('panmove', function(evt) {

				if(instance.mainOptions.mobileGesturesBehaviour == 'pinchPanCanvas') {

					var panX = 0,
						panY = 0;

					//left, right
					if(evt.direction == 2 || evt.direction == 4) {
						panX = evt.direction == 2 ? -1 : 1;
					}
					//up, down
					else if(evt.direction == 8 || evt.direction == 16) {
						panY = evt.direction == 8 ? -1 : 1;
					}

					panX = panX * (Math.abs(evt.velocity) * 10);
					panY = panY * (Math.abs(evt.velocity) * 10);

					instance.currentViewInstance.stage.relativePan(new fabric.Point(panX, panY));

				}

			});
		}

		/**
	     * Gets fired as soon as the product designer is ready to receive API calls.
	     *
	     * @event FancyProductDesigner#ready
	     * @param {Event} event
	     */
		$elem.trigger('ready');

		$window.resize();

	};

	var _createProductJSONFromHTML = function($products) {

		if($products.length) {
			FPDUtil.log('FPD: Creating products from HTML is deprecated, use a JSON file to store your products. Please check out the documentation!', 'warn');
		}

		var producJSON = [];

		//creates all products from HTML markup
		var _createProductsFromHTML = function($products, category) {

			for(var i=0; i < $products.length; ++i) {

				//get other views
				var views = $($products.get(i)).children('.fpd-product');
				//get first view
				views.splice(0,0,$products.get(i));

				var viewsArr = [];
				views.each(function(j, view) {

					var $view = $(view);
					var viewObj = {
						title: view.title,
						thumbnail: $view.data('thumbnail'),
						elements: [],
						options: $view.data('options')
					};

					viewObj.mask = $view.data('mask') ? $view.data('mask') : null;

					if(j === 0) {

						//get product title from first view
						if($view.data('producttitle')) {
							viewObj.productTitle = $view.data('producttitle');
						}

						//get product thumbnail from first view
						if($view.data('productthumbnail')) {
							viewObj.productThumbnail = $view.data('productthumbnail');
						}

					}

					$view.children('img,span').each(function(k, element) {

						var $element = $(element),
							source;

						if($element.is('img')) {
							source = $element.data('src') == undefined ? $element.attr('src') : $element.data('src');
						}
						else {
							source = $element.text()
						}

						var elementObj = {
							type: $element.is('img') ? 'image' : 'text', //type
							source: source, //source
							title: $element.attr('title'),  //title
							parameters: $element.data('parameters') == undefined || $element.data('parameters').length <= 2 ? {} : $element.data('parameters')  //parameters
						};

						viewObj.elements.push(elementObj);

					});

					viewsArr.push(viewObj);

				});

				//add product in category or asn own item
				if(category) {

					//get category index by category name
					var catIndex =  $.map(producJSON, function(obj, index) {
					    if(obj.category == category) {
					        return index;
					    }
					}).shift();

					if(isNaN(catIndex)) { //category does not exist in products
						catIndex = producJSON.length; // set index
						producJSON.push({category: category, products: []});
					}

					producJSON[catIndex].products.push(viewsArr);

				}
				else { //no categories
					producJSON.push(viewsArr)
				}

			}


		};

		//check if categories are used
		if($products.is('.fpd-category') && $products.filter('.fpd-category').length > 1) {

			//loop through all categories
			$products.each(function(i, cat) {
				var $cat = $(cat);
				_createProductsFromHTML($cat.children('.fpd-product'), $cat.attr('title'));
			});

		}
		else { //no categories are used

			//check if only one category is used
			$products = $products.filter('.fpd-category').length === 0 ? $products : $products.children('.fpd-product');
			_createProductsFromHTML($products, false);

		}

		if(producJSON.length > 0) {
			instance.setupProducts(producJSON);
		}

	};

	var _createDesignJSONFromHTML = function($designs) {

		var _loopDesignCategory = function($designCategories, pushToCat) {

			$designCategories.each(function(index, cat) {

				var $category = $(cat),
					categoryObj = {title: $category.attr('title'), thumbnail: $category.data('thumbnail')};

				if($category.data('parameters')) {
					categoryObj.parameters = $category.data('parameters');
				}

				pushToCat ? pushToCat.push(categoryObj) : instance.designs.push(categoryObj);

				if($category.children('.fpd-category').length > 0) {

					categoryObj.category = [];
					_loopDesignCategory($category.children('.fpd-category'), categoryObj.category);

				}
				else {

					var designImages = [];

					$category.children('img').each(function(designIndex, img) {

						var $img = $(img),
							designObj = {
								source: $img.data('src') === undefined ? $img.attr('src') : $img.data('src'),
								title: $img.attr('title'),
								parameters: $img.data('parameters'),
								thumbnail: $img.data('thumbnail')
							};

						designImages.push(designObj);

					});

					categoryObj.designs = designImages;

				}

			});

		};

		if($designs.length > 0) {

			FPDUtil.log('FPD: Creating designs from HTML is deprecated, use a JSON file to store your products. Please check out the documentation!', 'warn');

			//check if categories are used or first category also includes sub-cats
			if($designs.filter('.fpd-category').length > 1 || $designs.filter('.fpd-category:first').children('.fpd-category').length > 0) {

				_loopDesignCategory($designs.filter('.fpd-category'));

			}
			else { //display single category or designs without categories

				var $designImages = $designs;
				if($designImages.hasClass('fpd-category')) {
					$designImages = $designImages.children('img');
				}

				$designImages.each(function(designIndex, img) {

					var $img = $(img),
						designObj = {
							source: $img.data('src') === undefined ? $img.attr('src') : $img.data('src'),
							title: $img.attr('title'),
							parameters: $img.data('parameters'),
							thumbnail: $img.data('thumbnail')
						};

					instance.designs.push(designObj);

				});

			}

			$designs.remove();

			instance.setupDesigns(instance.designs);
		}

	};

	//get category index by category name
	var _getCategoryIndexInProducts = function(catName) {

		var catIndex =  $.map(instance.products, function(obj, index) {
		    if(obj.category == catName) {
		        return index;
		    }
		}).shift();

		return isNaN(catIndex) ? false : catIndex;

	};

	var _toggleUndoRedoBtn = function(undos, redos) {

		if(undos.length === 0) {
		  	instance.$actionsWrapper.find('[data-action="undo"]').addClass('fpd-disabled');
  		}
  		else {
	  		instance.$actionsWrapper.find('[data-action="undo"]').removeClass('fpd-disabled');
  		}

  		if(redos.length === 0) {
	  		instance.$actionsWrapper.find('[data-action="redo"]').addClass('fpd-disabled');
  		}
  		else {
	  		instance.$actionsWrapper.find('[data-action="redo"]').removeClass('fpd-disabled');
  		}

	};

	var _updateEditorBox = function(element) {

		if($editorBox === null) {
			return;
		}

		$editorBox.children('div').empty();
		$editorBox.children('h5').text(element.title);

		for(var i=0; i < instance.mainOptions.editorBoxParameters.length; ++i) {

			var parameter = instance.mainOptions.editorBoxParameters[i],
				value = element[parameter];

			if(value !== undefined) {

				value = typeof value === 'number' ? value.toFixed(2) : value;
				value = (typeof value === 'object' && value.source) ? value.source.src : value;
				if(parameter === 'fill' && element.type === FPDPathGroupName) {
					value = element.svgFill;
				}

				$editorBox.children('div').append('<p><i>'+parameter+'</i>: <input type="text" value="'+value+'" readonly /></p>');

			}

		}

	};

	var _onViewCreated = function() {

		//add all views of product till views end is reached
		if(instance.viewInstances.length < instance.currentViews.length) {

			instance.addView(instance.currentViews[instance.viewInstances.length]);

		}
		//all views added
		else {

			$elem.off('viewCreate', _onViewCreated);

			instance.toggleSpinner(false);
			instance.selectView(0);

			//search for object with auto-select
			if(!instance.mainOptions.editorMode && instance.currentViewInstance && $(instance.currentViewInstance.stage.getElement()).is(':visible')) {
				var viewElements = instance.currentViewInstance.stage.getObjects(),
					selectElement = null;

				for(var i=0; i < viewElements.length; ++i) {
					var obj = viewElements[i];

					 if(obj.autoSelect && !obj.hasUploadZone) {
					 	selectElement = obj;
					 }

				}
			}

			if(selectElement && instance.currentViewInstance) {
				setTimeout(function() {

					instance.currentViewInstance.stage.setActiveObject(selectElement);
					selectElement.setCoords();
					instance.currentViewInstance.stage.renderAll();

				}, 500);
			}

			instance.productCreated = true;

			//close dialog and off-canvas on element add
			if( instance.mainBar && instance.mainBar.__setup) {

				//instance.mainBar.toggleDialog(false);

			}

			if(instance.mainBar) {
				instance.mainBar.__setup = true; //initial active module fix
			}

			$window.resize();

			/**
		     * Gets fired as soon as a product has been fully added to the designer.
		     *
		     * @event FancyProductDesigner#productCreate
		     * @param {Event} event
		     * @param {array} currentViews - An array containing all views of the product.
		     */
			$elem.trigger('productCreate', [instance.currentViews]);

		}

	};

	var _updateElementTooltip = function() {

		var element = instance.currentElement;

		if(element && instance.$elementTooltip && instance.productCreated && !element.uploadZone && !element.__editorMode) {

			if(element.isOut && element.boundingBoxMode === 'inside') {
				instance.$elementTooltip.text(_outOfBoundingBoxLabel).show();
			}
			else if(instance.mainOptions.imageSizeTooltip && FPDUtil.getType(element.type) === 'image') {
				instance.$elementTooltip.text(parseInt(element.width * element.scaleX) +' x '+ parseInt(element.height * element.scaleY)).show();
			}
			else {
				instance.$elementTooltip.hide();
			}

			var oCoords = element.oCoords;
			instance.$elementTooltip.css({
				left: instance.$productStage.position().left + oCoords.mt.x,
				top: oCoords.mt.y - 10 + instance.$productStage.position().top
			});

		}
		else if(instance.$elementTooltip) {
			instance.$elementTooltip.hide();
		}

	};

	var _loadTemplate = function(template, type, loadIndex, callback) {

		var templateType = $.isArray(type) ? type[loadIndex] : type;

		$.get(
			instance.mainOptions.templatesDirectory+template+'.'+templateType,
			callback
		)
		.fail(function() {

			if($.isArray(type) && type[loadIndex+1]) {
				_loadTemplate(template, type, ++loadIndex, callback);
			}
			else {
				alert(instance.mainOptions.templatesDirectory+template+'.'+templateType+' could not be loaded.')
			}

		});

	};

	var _calculateViewsPrice = function() {

		instance.currentPrice = instance.singleProductPrice = 0;

		//calulate total price of all views
		for(var i=0; i < instance.viewInstances.length; ++i) {

			if(!instance.viewInstances[i].locked) {
				instance.singleProductPrice += instance.viewInstances[i].truePrice;
			}

		}
	};

	var _downloadRemoteImage = function(source, title, options) {

		options = options === undefined ? {} : options;

		var ajaxSettings = instance.mainOptions.customImageAjaxSettings,
			uploadsDir = (ajaxSettings.data && ajaxSettings.data.uploadsDir) ? ajaxSettings.data.uploadsDir : '',
			uploadsDirURL = (ajaxSettings.data && ajaxSettings.data.uploadsDirURL) ? ajaxSettings.data.uploadsDirURL : '',
			saveOnServer = ajaxSettings.data && ajaxSettings.data.saveOnServer ? 1 : 0;

		instance._loadingCustomImage = true;
		instance.toggleSpinner(true,  instance.getTranslation('misc', 'loading_image'));
		instance.$viewSelectionWrapper.addClass('fpd-disabled');

		var uploadAjaxSettings  = $.extend({}, ajaxSettings);
		uploadAjaxSettings.success = function(data) {

			if(data && data.error === undefined) {

				instance.addCustomImage(
					data.image_src,
					data.filename ? data.filename : title,
					options
				);

			}
			else {

				instance.toggleSpinner(false);
				FPDUtil.showModal(data.error);

			}

		};

		uploadAjaxSettings.data = {
			url: source,
			uploadsDir: uploadsDir,
			uploadsDirURL: uploadsDirURL,
			saveOnServer: saveOnServer
		};

		//ajax post
		$.ajax(uploadAjaxSettings)
		.fail(function(evt) {

			instance._loadingCustomImage = false;
			instance.toggleSpinner(false);
			FPDUtil.showModal(evt.statusText);

		});

		if(instance.productCreated && instance.mainOptions.hideDialogOnAdd &&
			instance.$container.hasClass('fpd-topbar') && instance.mainBar) {

			instance.mainBar.toggleDialog(false);

		}

	};

	this._addGridItemToStage = function($item, additionalOpts, viewIndex) {

		viewIndex = viewIndex === undefined ? instance.currentViewIndex : viewIndex;

		if(!instance.currentViewInstance) { return; }

		additionalOpts = additionalOpts === undefined ? {} : additionalOpts;

		var moduleType = $item.parents('.fpd-module:first').data('module'),
			title = $item.data('title') ? $item.data('title') : null;

		if(moduleType == 'designs') {

			var options = $.extend(
				{},
				$item.data('parameters') ? $item.data('parameters') : {},
				additionalOpts
			);

			instance._addCanvasDesign(
				$item.data('source'),
				$item.data('title'),
				options,
				viewIndex
			);

		}
		else {

			var options = $.extend(
				{},
				$item.data('options') ? $item.data('options') : {},
				{_addToUZ: instance.currentViewInstance.currentUploadZone},
				additionalOpts
			);

			instance._addCanvasImage(
				$item.data('source'),
				$item.data('title'),
				options,
				$item.parents('[data-context="upload"]').length == 0,
				viewIndex
			);

		}

	};

	this._addCanvasImage = function(source, title, options, isRemoteImage, viewIndex) {

		options = options === undefined ? {} : options;
		isRemoteImage = isRemoteImage === undefined ? false : isRemoteImage;

		if(!instance.currentViewInstance) { return; }

		var ajaxSettings = instance.mainOptions.customImageAjaxSettings,
			saveOnServer = ajaxSettings.data && ajaxSettings.data.saveOnServer ? 1 : 0;

		//download remote image to local server (FB, Insta, Pixabay, Depositphotos)
		if(saveOnServer && isRemoteImage) {

			_downloadRemoteImage(
				source,
				title,
				options
			);

		}
		//add data uri or local image to canvas
		else {

			instance._loadingCustomImage = true;
			instance.addCustomImage(
				source,
				title ,
				options,
				viewIndex
			);

		}

		if(instance.productCreated && instance.mainOptions.hideDialogOnAdd && instance.mainBar) {
			instance.mainBar.toggleDialog(false);
		}

	};

	this._addCanvasDesign = function(source, title, params, viewIndex) {

		params = params === undefined ? {} : params;

		if(!instance.currentViewInstance) { return; }

		instance.toggleSpinner(true, instance.getTranslation('misc', 'loading_image'));

		params = $.extend({}, instance.currentViewInstance.options.customImageParameters, params);

		params.isCustom = true;
		if(instance.currentViewInstance.currentUploadZone) {
			params._addToUZ = instance.currentViewInstance.currentUploadZone;
		}


		instance.addElement('image', source, title, params, viewIndex);

		if(instance.productCreated && instance.mainOptions.hideDialogOnAdd && instance.mainBar) {
			instance.mainBar.toggleDialog(false);
		}

	};

	/**
	 * Adds a new product to the product designer.
	 *
	 * @method addProduct
	 * @param {array} views An array containing the views for a product. A view is an object with a title, thumbnail and elements property. The elements property is an array containing one or more objects with source, title, parameters and type.
	 * @param {string} [category] If categories are used, you need to define the category title.
	 */
	this.addProduct = function(views, category) {

		var catIndex = _getCategoryIndexInProducts(category);

		/*views.forEach(function(view) {
			view.options = view.options === undefined && typeof view.options !== 'object' ? instance.mainOptions : fpdOptionsInstance.merge(instance.mainOptions, view.options)
		});*/

		if(category === undefined) {
			instance.products.push(views);
		}
		else {

			if(catIndex === false) {

				catIndex = instance.products.length;
				instance.products[catIndex] = {category: category, products: []};

			}

			instance.products[catIndex].products.push(views);

		}

		/**
		 * Gets fired when a product is added.
		 *
		 * @event FancyProductDesigner#productAdd
		 * @param {Event} event
		 * @param {Array} views - The product views.
		 * @param {String} category - The category title.
		 * @param {Number} catIndex - The index of the category.
		 */
		$elem.trigger('productAdd', [views, category, catIndex]);

	};

	/**
	 * Selects a product by index and category index.
	 *
	 * @method selectProduct
	 * @param {number} index The requested product by an index value. 0 will load the first product.
	 * @param {number} [categoryIndex] The requested category by an index value. 0 will load the first category.
	 * @example fpd.selectProduct( 1, 2 ); //will load the second product from the third category
	 */
	this.selectProduct = function(index, categoryIndex) {

		instance.currentCategoryIndex = categoryIndex === undefined ? instance.currentCategoryIndex : categoryIndex;

		var productsObj;
		if(instance.products && instance.products.length && instance.products[0].category) { //categories enabled
			var category = instance.products[instance.currentCategoryIndex];
			productsObj = category.products;
		}
		else { //no categories enabled
			productsObj = instance.products;
		}

		instance.currentProductIndex = index;
		if(index < 0) { currentProductIndex = 0; }
		else if(index > productsObj.length-1) { instance.currentProductIndex = productsObj.length-1; }

		var product = productsObj[instance.currentProductIndex];

		/**
		 * Gets fired when a product is selected.
		 *
		 * @event FancyProductDesigner#productSelect
		 * @param {Event} event
		 * @param {Number} index - The index of the product in the category.
		 * @param {Number} categoryIndex - The index of the category.
		 * @param {Object} product - An object containing the product (views).
		 */
		$elem.trigger('productSelect', [index, categoryIndex, product]);

		instance.loadProduct(product, instance.mainOptions.replaceInitialElements);

	};

	/**
	 * Loads a new product to the product designer.
	 *
	 * @method loadProduct
	 * @param {array} views An array containing the views for the product.
	 * @param {Boolean} [onlyReplaceInitialElements=false] If true, the initial elements will be replaced. Custom added elements will stay on the canvas.
	 * @param {Boolean} [mergeMainOptions=false] Merges the main options into every view options.
	 */
	this.loadProduct = function(views, replaceInitialElements, mergeMainOptions) {

		if(!views) { return; }

		instance._prevPrintingBoxes = [];
		this.viewInstances.forEach(function(viewInstance) {

			instance._prevPrintingBoxes.push(FPDUtil.objectHasKeys(viewInstance.options.printingBox, ['left','top','width','height']) ? viewInstance.options.printingBox : null);

		})

		replaceInitialElements = replaceInitialElements === undefined ? false : replaceInitialElements;
		mergeMainOptions = mergeMainOptions === undefined ? false : mergeMainOptions;

		if($stageLoader.is(':hidden')) {
			instance.toggleSpinner(true);
		}

		//reset when loading a product
		instance.productCreated = false;
		instance.colorLinkGroups = {};

		instance.globalCustomElements = [];
		if(replaceInitialElements) {
			instance.globalCustomElements = instance.getCustomElements();
		}
		else {
			instance.doUnsavedAlert = false;
		}

		instance.fixedElements = instance.getFixedElements();

		instance.reset();

		if(mergeMainOptions) {

			views.forEach(function(view, i) {
				view.options = fpdOptionsInstance.merge(instance.mainOptions, view.options);
			});

		}

		instance.currentViews = views;

		_totalProductElements = _productElementLoadingIndex = 0;
		views.forEach(function(view, i) {
			_totalProductElements += view.elements.length;
		});

		if(!instance.$viewSelectionWrapper) {

			instance.$viewSelectionWrapper = $('<div class="fpd-views-wrapper"><span class="fpd-view-prev"><span class="fpd-icon-forward"></span></span><div class="fpd-views-selection"></div><span class="fpd-view-next"><span class="fpd-icon-forward"></span></span></div>');

		}

		if(($elem.hasClass('fpd-views-outside') || $elem.hasClass('fpd-device-smartphone')) && !instance.mainOptions.modalMode) {
			$elem.after(instance.$viewSelectionWrapper);
		}
		else {
			instance.$mainWrapper.append(instance.$viewSelectionWrapper);
		}

		$elem.on('viewCreate', _onViewCreated);

		if(views) {
			instance.addView(views[0]);
		}

	};

	/**
	 * Adds a view to the current visible product.
	 *
	 * @method addView
	 * @param {object} view An object with title, thumbnail and elements properties.
	 */
	this.addView = function(view) {

		var viewImageURL = instance.mainOptions._loadFromScript ? instance.mainOptions._loadFromScript + view.thumbnail : view.thumbnail;

		instance.$viewSelectionWrapper.children('.fpd-views-selection')
		.append('<div class="fpd-shadow-1 fpd-item fpd-tooltip" title="'+view.title+'"><picture style="background-image: url('+viewImageURL+');"></picture></div>')
		.children('div:last').click(function(evt) {

			instance.selectView(instance.$viewSelectionWrapper.children('.fpd-views-selection').children('.fpd-item').index($(this)));

		});

		var mainOptions = $.extend(true, {}, instance.mainOptions);

		//remove unnecessary props that are not needed in view
		delete mainOptions['productsJSON'];
		delete mainOptions['designsJSON'];
		delete mainOptions['guidedTour'];
		delete mainOptions['fonts'];
		delete mainOptions['pricingRules'];
		delete mainOptions['hexNames'];
		delete mainOptions['customImageAjaxSettings'];
		delete mainOptions['colorPickerPalette'];
		delete mainOptions['imageEditorSettings'];
		delete mainOptions['mainBarModules'];

		view.options = view.options === undefined && typeof view.options !== 'object' ? mainOptions : $.extend(true, {}, fpdOptionsInstance.merge(mainOptions, view.options));


		var viewInstance = new FancyProductDesignerView(instance.$productStage, view, function(viewInstance) {

			//remove view instance if not added to product container
			if($(viewInstance.stage.wrapperEl).parent().length === 0) {
				viewInstance.reset();
				return;
			}

			if(instance.viewInstances.length == 0) {
				viewInstance.resetCanvasSize();
			}

			instance.viewInstances.push(viewInstance);
			/**
			 * Gets fired when a view is created.
			 *
			 * @event FancyProductDesigner#viewCreate
			 * @param {Event} event
			 * @param {FancyProductDesignerView} viewInstance
			 */
			$elem.trigger('viewCreate', [viewInstance]);

		}, instance.mainOptions.fabricCanvasOptions );


		viewInstance.stage.on({

			'object:scaling': function(opts) {
			},
			'object:moving': function(opts) {
			},
			'object:rotating': function(opts) {
			}

		});

		$(viewInstance)
		.on('beforeElementAdd', function(evt, type, source, title, params) {

			if(!instance.productCreated) {
				_productElementLoadingIndex++;

				var loadElementState = title + '<br>' + String(_productElementLoadingIndex) + '/' + _totalProductElements;
				$stageLoader.find('.fpd-loader-text').html(loadElementState);
			}

		})
		.on('canvas:mouseUp', function(evt, viewInstance) {

			if(instance.mainOptions.fabricCanvasOptions.allowTouchScrolling) {
				$elem.removeClass('fpd-disable-touch-scrolling');
				instance.currentViewInstance.stage.allowTouchScrolling = true;
			}

		})
		.on('canvas:mouseMove', function(evt, viewInstance, opts) {

			instance.mouseOverCanvas = opts.target ? opts.target : true;

		})
		.on('canvas:mouseOut', function(evt, viewInstance) {

			instance.mouseOverCanvas = false;

		})
		.on('elementAdd', function(evt, element) {

			if(!element) {
				instance.toggleSpinner(false);
				return;
			}

			if(instance.productCreated && FPDUtil.getType(element.type) == 'image' && element.isCustom) {
				instance.toggleSpinner(false);
				FPDUtil.showMessage(instance.getTranslation('misc', 'image_added'));
			}

			//check if element has a color linking group
			if(element.colorLinkGroup && element.colorLinkGroup.length > 0 && !instance.mainOptions.editorMode) {

				var viewIndex = this.getIndex();

				if(instance.colorLinkGroups.hasOwnProperty(element.colorLinkGroup)) { //check if color link object exists for the link group

					//add new element with id and view index of it
					instance.colorLinkGroups[element.colorLinkGroup].elements.push({id: element.id, viewIndex: viewIndex});

					if(typeof element.colors === 'object') {

						//create color group colors
						var colorGroupColors = instance.mainOptions.replaceColorsInColorGroup ? element.colors : instance.colorLinkGroups[element.colorLinkGroup].colors.concat(element.colors);
						instance.colorLinkGroups[element.colorLinkGroup].colors = FPDUtil.arrayUnique(colorGroupColors);

					}

				}
				else {

					//create initial color link object
					instance.colorLinkGroups[element.colorLinkGroup] = {elements: [{id:element.id, viewIndex: viewIndex}], colors: []};

					if(typeof element.colors === 'object') {

						instance.colorLinkGroups[element.colorLinkGroup].colors = element.colors;

					}

				}

			}

			//close dialog and off-canvas on element add
			if(instance.mainBar && instance.productCreated && instance.mainOptions.hideDialogOnAdd) {
				instance.mainBar.toggleDialog(false);

			}

			/**
			 * Gets fired when an element is added.
			 *
			 * @event FancyProductDesigner#elementAdd
			 * @param {Event} event
			 * @param {fabric.Object} element
			 */
			$elem.trigger('elementAdd', [element]);

			$elem.trigger('viewCanvasUpdate', [viewInstance]);

		})
		.on('boundingBoxToggle', function(evt, currentBoundingObject, addRemove) {

			/**
		     * Gets fired as soon as the bounding box is added to or removed from the stage.
		     *
		     * @event FancyProductDesigner#boundingBoxToggle
		     * @param {Event} event
		     * @param {fabric.Object} currentBoundingObject - A fabricJS rectangle representing the bounding box.
		     * @param {Boolean} addRemove - True=added, false=removed.
		     */
			$elem.trigger('boundingBoxToggle', [currentBoundingObject, addRemove]);

		})
		.on('elementSelect', function(evt, element) {

			instance.currentElement = element;

			if(element) {
				_updateElementTooltip();
			}
			else { //deselected

				if(instance.$elementTooltip) {
					instance.$elementTooltip.hide();
				}

				instance.$mainWrapper.children('.fpd-snap-line-h, .fpd-snap-line-v').hide();

			}
			/**
			 * Gets fired when an element is selected.
			 *
			 * @event FancyProductDesigner#elementSelect
			 * @param {Event} event
			 * @param {fabric.Object} element
			 */
			$elem.trigger('elementSelect', [element]);

		})
		.on('elementChange', function(evt, type, element) {

			_updateElementTooltip();
			_updateEditorBox(element.getBoundingRect());

			/**
			 * Gets fired when an element is changed.
			 *
			 * @event FancyProductDesigner#elementChange
			 * @param {Event} event
			 * @param {fabric.Object} element
			 */
			$elem.trigger('elementChange', [type, element]);

		})
		.on('elementModify', function(evt, element, parameters) {

			_updateElementTooltip();

			/**
			 * Gets fired when an element is modified.
			 *
			 * @event FancyProductDesigner#elementModify
			 * @param {Event} event
			 * @param {fabric.Object} element
			 * @param {Object} parameters
			 */
			$elem.trigger('elementModify', [element, parameters]);

			/**
			 * Gets fired when an element is modified.
			 *
			 * @event FancyProductDesigner#viewCanvasUpdate
			 * @param {Event} event
			 * @param {FancyProductDesignerView} viewInstance
			 */
			$elem.trigger('viewCanvasUpdate', [viewInstance]);

		})
		.on('undoRedoSet', function(evt, undos, redos) {

			instance.doUnsavedAlert = true;
			_toggleUndoRedoBtn(undos, redos);

			/**
			 * Gets fired when an undo or redo state is set.
			 *
			 * @event FancyProductDesigner#undoRedoSet
			 * @param {Event} event
			 * @param {Array} undos - Array containing all undo objects.
			 * @param {Array} redos - Array containing all redo objects.
			 */
			$elem.trigger('undoRedoSet', [undos, redos]);

		})
		.on('priceChange', function(evt, price, viewPrice) {

			var truePrice = instance.calculatePrice();

			/**
		     * Gets fired as soon as the price changes in a view.
		     *
		     * @event FancyProductDesigner#priceChange
		     * @param {Event} event
		     * @param {number} elementPrice - The price of the element.
		     * @param {number} totalPrice - The true price of all views with quantity.
		     * @param {number} singleProductPrice - The true price of all views without quantity.
		     */
			$elem.trigger('priceChange', [price, truePrice, instance.singleProductPrice]);

		})
		.on('elementCheckContainemt', function(evt, element, boundingBoxMode) {

			if(boundingBoxMode === 'inside') {

				_updateElementTooltip();

			}

		})
		.on('elementColorChange', function(evt, element, hex, colorLinking) {

			if(instance.productCreated && colorLinking && element.colorLinkGroup && element.colorLinkGroup.length > 0) {

				var group = instance.colorLinkGroups[element.colorLinkGroup];
				if(group && group.elements) {
					for(var i=0; i < group.elements.length; ++i) {

						var id = group.elements[i].id,
							viewIndex = group.elements[i].viewIndex,
							target = instance.getElementByID(id, viewIndex);

						if(target && target !== element && hex) {
							instance.viewInstances[viewIndex].changeColor(target, hex, false);
						}

					}
				}

			}

			/**
			 * Gets fired when the color of an element is changed.
			 *
			 * @event FancyProductDesigner#elementColorChange
			 * @param {Event} event
			 * @param {fabric.Object} element
			 * @param {String} hex Hexadecimal color string.
			 * @param {Boolean} colorLinking Color of element is linked to other colors.
			 */
			$elem.trigger('elementColorChange', [element, hex, colorLinking]);
			$elem.trigger('viewCanvasUpdate', [viewInstance]);

		})
		.on('elementRemove', function(evt, element) {

			//delete fixed element
			var deleteIndex = instance.fixedElements.findIndex(function(item) {
				return item.element.title == element.title
			})

			if(deleteIndex != -1) {
				instance.fixedElements.splice(deleteIndex, 1);
			}

			/**
		     * Gets fired as soon as an element has been removed.
		     *
		     * @event FancyProductDesigner#elementRemove
		     * @param {Event} event
		     * @param {fabric.Object} element - The fabric object that has been removed.
		     */
			$elem.trigger('elementRemove', [element]);

			$elem.trigger('viewCanvasUpdate', [viewInstance]);

		})
		.on('fabricObject:added fabricObject:removed', function(evt, element) {

			$elem.trigger(evt.type, [element]);

		})
		.on('textEditEnter', function() {

			if(instance.currentElement) {
				instance.toolbar.updatePosition(instance.currentElement);
			}

		})

		viewInstance.setup();

		FPDUtil.updateTooltip();

		instance.$viewSelectionWrapper.children('.fpd-views-selection').children().length > 1 ? instance.$viewSelectionWrapper.show() : instance.$viewSelectionWrapper.hide();

	};

	/**
	 * Selects a view from the current visible views.
	 *
	 * @method selectView
	 * @param {number} index The requested view by an index value. 0 will load the first view.
	 */
	this.selectView = function(index) {

		if(instance.viewInstances.length <= 0) {return;}

		instance.resetZoom();

		instance.currentViewIndex = index;
		if(index < 0) { instance.currentViewIndex = 0; }
		else if(index > instance.viewInstances.length-1) { instance.currentViewIndex = instance.viewInstances.length-1; }

		instance.$viewSelectionWrapper.children('.fpd-views-selection').children('div').removeClass('fpd-view-active')
		.eq(index).addClass('fpd-view-active');

		instance.$mainWrapper.children('.fpd-ruler').remove();

		if(instance.currentViewInstance) {
			//delete all undos/redos
			instance.currentViewInstance.undos = [];
			instance.currentViewInstance.redos = [];

			//remove some objects
			var removeObjs = ['_snap_lines_group', '_ruler_hor', '_ruler_ver'];
			for(var i=0; i<removeObjs.length; ++i) {
				var removeObj = instance.currentViewInstance.getElementByID(removeObjs[i]);
				if(removeObj) {
					instance.currentViewInstance.stage.remove(removeObj);
				}
			}

			instance.currentViewInstance._snapElements = false;

		}

		instance.currentViewInstance = instance.viewInstances[instance.currentViewIndex];

		instance.deselectElement();

		//select view wrapper and render stage of view
		instance.$productStage.children('.fpd-view-stage').addClass('fpd-hidden').eq(instance.currentViewIndex).removeClass('fpd-hidden');
		instance.currentViewInstance.stage.renderAll();

		//toggle custom adds
		if($mainBar && $mainBar.find('.fpd-navigation').length) {

			var viewOpts = instance.currentViewInstance.options,
				$nav = $mainBar.find('.fpd-navigation');

			$nav.children('[data-module="designs"]').toggleClass('fpd-disabled', !viewOpts.customAdds.designs);
			$('.fpd-sc-module-wrapper [data-module="designs"]').toggleClass('fpd-disabled', !viewOpts.customAdds.designs);
			$nav.children('[data-module="images"]').toggleClass('fpd-disabled', !viewOpts.customAdds.uploads);
			$('.fpd-sc-module-wrapper [data-module="images"]').toggleClass('fpd-disabled', !viewOpts.customAdds.designs);
			$nav.children('[data-module="text"]').toggleClass('fpd-disabled', !viewOpts.customAdds.texts);
			$('.fpd-sc-module-wrapper [data-module="text"]').toggleClass('fpd-disabled', !viewOpts.customAdds.designs);

			//PLUS
			if(typeof FPDNamesNumbersModule !== 'undefined') {
				$nav.children('[data-module="names-numbers"]').toggleClass('fpd-disabled', !instance.currentViewInstance.textPlaceholder && !instance.currentViewInstance.numberPlaceholder);
			}
			$nav.children('[data-module="drawing"]').toggleClass('fpd-disabled', !viewOpts.customAdds.drawing);

			//select nav item, if sidebar layout is used, no active item is set and active item is not disabled
			if($elem.hasClass('fpd-device-desktop')) {

				if($elem.hasClass('fpd-sidebar')) {

					if(($nav.children('.fpd-active').length === 0) || $nav.children('.fpd-active').hasClass('fpd-disabled')) {

						$nav.children(':not(.fpd-disabled)').length > 0 ? $nav.children(':not(.fpd-disabled)').first().click() : instance.mainBar.$content.children('.fpd-module').removeClass('fpd-active');

					}
					else if(instance.mainBar.$content.children('.fpd-active').length == 0 && instance.productCreated) {
						$nav.children(':first').click()
					}

				}
				else if($elem.hasClass('fpd-topbar')) {

					if($nav.children('.fpd-active').hasClass('fpd-disabled')) {

						instance.mainBar.toggleDialog(false);
					}

				}

			}

			//if products module is hidden and selected, select next
			if(instance.$container.hasClass('fpd-products-module-hidden') && $nav.children('.fpd-active').filter('[data-module="products"]').length > 0) {
				$nav.children(':not(.fpd-disabled)').eq(1).click();
			}

		}

		//adjust off-canvas height to view height
		if(instance.mainBar && instance.mainBar.$content && instance.$container.filter('[class*="fpd-off-canvas-"]').length > 0) {
			instance.mainBar.$content.height(instance.$mainWrapper.height());
		}

		_toggleUndoRedoBtn(instance.currentViewInstance.undos, instance.currentViewInstance.redos);

		//toggle view locker
		instance.$mainWrapper.children('.fpd-modal-lock')
		.removeClass('fpd-animated')
		.toggleClass('fpd-active', instance.currentViewInstance.options.optionalView)
		.toggleClass('fpd-unlocked', !instance.currentViewInstance.locked);
		setTimeout(function() {
			instance.$mainWrapper.children('.fpd-modal-lock').addClass('fpd-animated');
		}, 1);

		//reset view canvas size
		instance.$productStage.width(instance.currentViewInstance.options.stageWidth);
		instance.currentViewInstance.resetCanvasSize();
		instance.currentViewInstance.resetCanvasSize(); //fix: to calculate correct size when views have different dimensions


		/**
	     * Gets fired as soon as a view has been selected.
	     *
	     * @event FancyProductDesigner#viewSelect
	     * @param {Event} event
	     * @param {Number} viewIndex
	     * @param {Object} viewInstance
	     */
		$elem.trigger('viewSelect', [instance.currentViewIndex, instance.currentViewInstance]);

	};

	/**
	 * Adds a new element to the product designer.
	 *
	 * @method addElement
	 * @param {string} type The type of an element you would like to add, 'image' or 'text'.
	 * @param {string} source For image the URL to the image and for text elements the default text.
	 * @param {string} title Only required for image elements.
	 * @param {object} [parameters={}] An object with the parameters, you would like to apply on the element.
	 * @param {number} [viewIndex] The index of the view where the element needs to be added to. If no index is set, it will be added to current showing view.
	 */
	this.addElement = function(type, source, title, parameters, viewIndex) {

		parameters = parameters === undefined ? {} : parameters;

		viewIndex = viewIndex === undefined ? instance.currentViewIndex : viewIndex;

		instance.viewInstances[viewIndex].addElement(type, source, title, parameters);

		//element should be replaced in all views
		if(parameters.replace && parameters.replaceInAllViews) {

			for(var i=0; i < instance.viewInstances.length; ++i) {

				var viewInstance = instance.viewInstances[i];
				//check if not current view and view has at least one element with the replace value
				if(viewIndex !== i && viewInstance.getElementByReplace(parameters.replace) !== null) {
					viewInstance.addElement(type, source, title, parameters, i);
				}

			}

		}

	};

	/**
	 * Sets the parameters for a specified element.
	 *
	 * @method setElementParameters
	 * @param {object} parameters An object with the parameters that should be applied to the element.
	 * @param {fabric.Object | string} [element] A fabric object or the title of an element. If not set, the current selected element is used.
	 * @param {Number} [viewIndex] The index of the view you would like target. If not set, the current showing view will be used.
	 */
	this.setElementParameters = function(parameters, element, viewIndex) {

		element = element === undefined ? instance.stage.getActiveObject() : element;
		viewIndex = viewIndex === undefined ? instance.currentViewIndex : viewIndex;

		if(!element || parameters === undefined) {
			return false;
		}

		instance.viewInstances[viewIndex].setElementParameters(parameters, element);

	};

	/**
	 * Clears the product stage and resets everything.
	 *
	 * @method reset
	 */
	this.reset = function() {

		if(instance.currentViews === null) { return; }

		$elem.off('viewCreate', _onViewCreated);

		instance.deselectElement();
		instance.resetZoom();
		instance.currentViewIndex = instance.currentPrice = instance.singleProductPrice = instance.pricingRulesPrice = 0;
		instance.currentViewInstance = instance.currentViews = instance.currentElement = null;

		instance.viewInstances.forEach(function(view) {
			view.stage.clear();
		});

		instance.$mainWrapper.find('.fpd-view-stage').remove();
		$body.find('.fpd-views-selection').children().remove();

		instance.viewInstances = [];

		/**
	     * Gets fired as soon as the stage has been cleared.
	     *
	     * @event FancyProductDesigner#clear
	     * @param {Event} event
	     */
		$elem.trigger('clear');
		$elem.trigger('priceChange', [0, 0, 0]);
		stageCleared = true;

	};

	/**
	 * Deselects the selected element of the current showing view.
	 *
	 * @method deselectElement
	 */
	this.deselectElement = function() {

		if(instance.currentViewInstance) {

			instance.currentViewInstance.deselectElement();

		}

	};

	/**
	 * Creates all views in one data URL. The different views will be positioned below each other.
	 *
	 * @method getProductDataURL
	 * @param {Function} callback A function that will be called when the data URL is created. The function receives the data URL.
	 * @param {String} [backgroundColor=transparent] The background color as hexadecimal value. For 'png' you can also use 'transparent'.
	 * @param {Object} [options={}] See fabricjs documentation http://fabricjs.com/docs/fabric.Canvas.html#toDataURL.
	 * @param {Boolean} [options.onlyExportable=false] If true elements with excludeFromExport=true are not exported in the image.
	 * @param {Array} viewRange An array defining the start and the end indexes of the exported views.
	 * @example fpd.getProductDataURL( function(dataURL){} );
	 */
	this.getProductDataURL = function(callback, backgroundColor, options, viewRange) {

		callback = callback === undefined ? function() {} : callback ;
		backgroundColor = backgroundColor === undefined ? 'transparent' : backgroundColor;
		options = options === undefined ? {} : options;
		options.onlyExportable = options.onlyExportable === undefined ? false : options.onlyExportable;
		options.enableRetinaScaling = options.enableRetinaScaling === undefined ? false : options.enableRetinaScaling;
		viewRange = viewRange === undefined ? [] : viewRange;

		if(instance.viewInstances.length === 0) { callback(''); return; }

		instance.resetZoom();

		$body.append('<canvas id="fpd-hidden-canvas"></canvas>');

		var tempDevicePixelRatio = fabric.devicePixelRatio,
			printCanvas = new fabric.Canvas('fpd-hidden-canvas', {
				containerClass: 'fpd-hidden fpd-hidden-canvas',
				enableRetinaScaling: false
			}),
			viewCount = 0,
			multiplier = options.multiplier ? options.multiplier : 1,
			targetViews = viewRange.length == 2 ? instance.viewInstances.slice(viewRange[0], viewRange[1]) : instance.viewInstances;

		function _addCanvasImage(viewInstance) {

			fabric.devicePixelRatio = 1;

			viewInstance.toDataURL(function(dataURL) {

				fabric.Image.fromURL(dataURL, function(img) {

					printCanvas.add(img);

					if(viewCount > 0) {
						img.set('top', printCanvas.getHeight());
						printCanvas.setDimensions({height: (printCanvas.getHeight() + (viewInstance.options.stageHeight * multiplier))});
					}

					viewCount++;
					if(viewCount < targetViews.length) {
						_addCanvasImage(targetViews[viewCount]);
					}
					else {

						delete options['multiplier'];

						setTimeout(function() {

							callback(printCanvas.toDataURL(options));
							fabric.devicePixelRatio = tempDevicePixelRatio;
							printCanvas.dispose();
							$body.children('#fpd-hidden-canvas').remove();

							if(instance.currentViewInstance) {
								instance.currentViewInstance.resetCanvasSize();
							}

						}, 100);

					}

				}, {crossOrigin: "anonymous"});

			}, backgroundColor, options, instance.watermarkImg);

			if(viewInstance.options.stageWidth * multiplier > printCanvas.getWidth()) {
				printCanvas.setDimensions({width: viewInstance.options.stageWidth * multiplier});
			}



		};

		var firstView = targetViews[0];
		printCanvas.setDimensions({width: firstView.options.stageWidth * multiplier, height: firstView.options.stageHeight * multiplier});
		_addCanvasImage(firstView);

	};

	/**
	 * Gets the views as data URL.
	 *
	 * @method getViewsDataURL
	 * @param {Function} callback A function that will be called when the data URL is created. The function receives the data URL.
	 * @param {string} [backgroundColor=transparent] The background color as hexadecimal value. For 'png' you can also use 'transparent'.
	 * @param {string} [options={}] See fabricjs documentation http://fabricjs.com/docs/fabric.Canvas.html#toDataURL.
	 * @param {Boolean} [options.onlyExportable=false] If true elements with excludeFromExport=true are not exported in the image.
	 * @return {array} An array with all views as data URLs.
	 */
	this.getViewsDataURL = function(callback, backgroundColor, options) {

		callback = callback === undefined ? function() {} : callback;
		backgroundColor = backgroundColor === undefined ? 'transparent' : backgroundColor;
		options = options === 'undefined' ? {} : options;

		var dataURLs = [];

		instance.resetZoom();
		for(var i=0; i < instance.viewInstances.length; ++i) {

			instance.viewInstances[i].toDataURL(function(dataURL) {

				dataURLs.push(dataURL);

				if(dataURLs.length === instance.viewInstances.length) {
					callback(dataURLs);
				}

			}, backgroundColor, options, instance.watermarkImg);

		}

	};

	/**
	 * Returns the views as SVG.
	 *
	 * @method getViewsSVG
	 * @param {Object} options See http://fabricjs.com/docs/fabric.StaticCanvas.html#toSVG.
	 * @param {Function} reviver See http://fabricjs.com/docs/fabric.StaticCanvas.html#toSVG.
	 * @return {array} An array with all views as SVG.
	 */
	this.getViewsSVG = function(options, reviver, respectPrintingBox) {

		var SVGs = [];

		for(var i=0; i < instance.viewInstances.length; ++i) {
			SVGs.push(instance.viewInstances[i].toSVG(options, reviver, respectPrintingBox, null, this.getUsedFonts()));
		}

		return SVGs;

	};

	/**
	 * Shows or hides the spinner with an optional message.
	 *
	 * @method toggleSpinner
	 * @param {String} state The state can be "show" or "hide".
	 * @param {Boolean} msg The message that will be displayed underneath the spinner.
	 * @return {jQuery} $stageLoader jQuery object containing the stage loader.
	 */
	this.toggleSpinner = function(state, msg) {

		state = state === undefined ? true : state;
		msg = msg === undefined ? '' : msg;

		if(state) {

			$stageLoader.fadeIn(300).find('.fpd-loader-text').html(msg);

		}
		else {

			$stageLoader.stop().fadeOut(300);

		}

		return $stageLoader;

	};

	/**
	 * Returns an fabric object by title.
	 *
	 * @method getElementByTitle
	 * @param {String} title The title of an element.
	 * @param {Number} [viewIndex=-1] The index of the target view. By default all views are scanned.
	 * @return {fabric.Object} FabricJS Object.
	 */
	this.getElementByTitle = function(title, viewIndex) {

		viewIndex === undefined ? -1 : viewIndex;

		var searchedElement = false;
		this.getElements(viewIndex, 'all', false).some(function(element) {

			if(element.title == title) {
				searchedElement = element;
				return true;
			}

		});

		return searchedElement;

	};

	/**
	 * Returns an array with fabricjs objects.
	 *
	 * @method getElements
	 * @param {Number} [viewIndex=-1] The index of the target view. By default all views are target.
	 * @param {String} [elementType='all'] The type of elements to return. By default all types are returned. Possible values: text, image.
	 * @param {String} [deselectElement=true] Deselect current selected element.
	 * @return {Array} An array containg the elements.
	 */
	this.getElements = function(viewIndex, elementType, deselectElement) {

		viewIndex = viewIndex === undefined || isNaN(viewIndex) ? -1 : viewIndex;
		elementType = elementType === undefined ? 'all' : elementType;
		deselectElement = deselectElement === undefined ? true : deselectElement;

		if(deselectElement) {
			this.deselectElement();
		}

		var allElements = [];
		if(viewIndex === -1) {

			for(var i=0; i < instance.viewInstances.length; ++i) {
				allElements = allElements.concat(instance.viewInstances[i].stage.getObjects());
			}

		}
		else {

			if(instance.viewInstances[viewIndex]) {
				allElements = instance.viewInstances[viewIndex].stage.getObjects();
			}
			else {
				return [];
			}

		}

		//remove bounding-box and printing-box object
		allElements = allElements.filter(function(obj) {
			return !obj._ignore;
		});

		if(elementType === 'text') {

			var textElements = [];
			allElements.forEach(function(elem) {

				if(FPDUtil.getType(elem.type) === 'text') {
					textElements.push(elem);
				}

			});

			return textElements;

		}
		else if(elementType === 'image') {

			var imageElements = [];
			allElements.forEach(function(elem) {

				if(FPDUtil.getType(elem.type) === 'image') {
					imageElements.push(elem);
				}

			});

			return imageElements;

		}

		return allElements;

	};

	/**
	 * Opens the current showing product in a Pop-up window and shows the print dialog.
	 *
	 * @method print
	 */
	this.print = function() {

		var _createPopupImage = function(dataURLs) {

			var images = [],
				imageLoop = 0;

			//load all images first
			for(var i=0; i < dataURLs.length; ++i) {

				var image = new Image();
				image.src = dataURLs[i];
				image.onload = function() {

					images.push(this);
					imageLoop++;

					//add images to popup and print popup
					if(imageLoop == dataURLs.length) {

						var popup = window.open('','','width='+images[0].width+',height='+(images[0].height*dataURLs.length)+',location=no,menubar=no,scrollbars=yes,status=no,toolbar=no');
						FPDUtil.popupBlockerAlert(popup, instance);

						popup.document.title = "Print Image";
						for(var j=0; j < images.length; ++j) {
							$(popup.document.body).append('<img src="'+images[j].src+'" />');
						}

						setTimeout(function() {
							popup.print();
						}, 1000);

					}
				}

			}

		};

		instance.getViewsDataURL(_createPopupImage);

	};

	/**
	 * Creates an image of the current showing product.
	 *
	 * @method createImage
	 * @param {boolean} [openInBlankPage= true] Opens the image in a Pop-up window.
	 * @param {boolean} [forceDownload=false] Downloads the image to the user's computer.
	 * @param {string} [backgroundColor=transparent] The background color as hexadecimal value. For 'png' you can also use 'transparent'.
	 * @param {string} [options] See fabricjs documentation http://fabricjs.com/docs/fabric.Canvas.html#toDataURL.
	 * @param {Boolean} [options.onlyExportable=false] If true elements with excludeFromExport=true are not exported in the image.
	 * @param {Boolean} [onlyCurrentView] If true only the curent showing view will be exported.
	 */
	this.createImage = function(openInBlankPage, forceDownload, backgroundColor, options, onlyCurrentView) {

		openInBlankPage = openInBlankPage === undefined ? true : openInBlankPage;
		forceDownload = forceDownload === undefined ? false : forceDownload;
		backgroundColor = backgroundColor === undefined ? 'transparent' : backgroundColor;
		options = options === undefined ? {} : options;
		onlyCurrentView = onlyCurrentView === undefined ? false : onlyCurrentView;

		var format = options.format === undefined ? 'png' : options.format;


		var _createPopupImage = function(dataURL) {

			var image = new Image();
			image.src = dataURL;

			image.onload = function() {

				if(openInBlankPage) {

					var popup = window.open('','_blank');
					FPDUtil.popupBlockerAlert(popup, instance);

					popup.document.title = "Product Image";
					$(popup.document.body).append('<img src="'+this.src+'" download="product.'+format+'" />');

					if(forceDownload) {
						window.location.href = popup.document.getElementsByTagName('img')[0].src.replace('image/'+format+'', 'image/octet-stream');
					}
				}

			}

		}

		onlyCurrentView ? instance.currentViewInstance.toDataURL(_createPopupImage, backgroundColor, options) : instance.getProductDataURL(_createPopupImage, backgroundColor, options);

	};

	/**
	 * Sets the zoom of the stage. 1 is equal to no zoom.
	 *
	 * @method setZoom
	 * @param {number} value The zoom value.
	 */
	this.setZoom = function(value) {

		//fix for android browser, because keyboard trigger resize event
		if(inTextField) {
			return;
		}

		zoomReseted = false;
		this.deselectElement();

		if(instance.currentViewInstance) {

			var responsiveScale = instance.currentViewInstance.responsiveScale;

			var point = new fabric.Point(instance.currentViewInstance.stage.getWidth() * 0.5, instance.currentViewInstance.stage.getHeight() * 0.5);

			instance.currentViewInstance.stage.zoomToPoint(point, value * responsiveScale);

			if(value == 1) {
				instance.resetZoom();
			}

		}


	};

	/**
	 * Resets the zoom.
	 *
	 * @method resetZoom
	 */
	this.resetZoom = function() {

		zoomReseted = true;
		this.deselectElement();

		if(instance.currentViewInstance) {

			var responsiveScale = instance.currentViewInstance.responsiveScale;

			instance.currentViewInstance.stage.zoomToPoint(new fabric.Point(0, 0), responsiveScale);
			instance.currentViewInstance.stage.absolutePan(new fabric.Point(0, 0));

		}

	};

	/**
	 * Get an elment by ID.
	 *
	 * @method getElementByID
	 * @param {Number} id The id of an element.
	 * @param {Number} [viewIndex] The view index you want to search in. If no index is set, it will use the current showing view.
	 */
	this.getElementByID = function(id, viewIndex) {

		viewIndex = viewIndex === undefined ? instance.currentViewIndex : viewIndex;

		return instance.viewInstances[viewIndex] ? instance.viewInstances[viewIndex].getElementByID(id) : null;

	};

	/**
	 * Returns the current showing product with all views and elements in the views.
	 *
	 * @method getProduct
	 * @param {boolean} [onlyEditableElements=false] If true, only the editable elements will be returned.
	 * @param {boolean} [customizationRequired=false] To receive the product the user needs to customize the initial elements.
	 * @return {array} An array with all views. A view is an object containing the title, thumbnail, custom options and elements. An element object contains the title, source, parameters and type.
	 */
	this.getProduct = function(onlyEditableElements, customizationRequired) {

		onlyEditableElements = onlyEditableElements === undefined ? false : onlyEditableElements;
		customizationRequired = customizationRequired === undefined ? false : customizationRequired;

		var customizationChecker = false,
			jsMethod = instance.mainOptions.customizationRequiredRule == 'all' ? 'every' : 'some';

		customizationChecker = instance.viewInstances[jsMethod](function(viewInst) {
			return viewInst.isCustomized;
		})

		if(customizationRequired && !customizationChecker) {
			FPDUtil.showMessage(instance.getTranslation('misc', 'customization_required_info'));
			return false;
		}

		this.deselectElement();
		this.resetZoom();

		instance.doUnsavedAlert = false;

		//check if an element is out of his containment
		var viewElements = this.getElements(),
			product = [];
		viewElements.forEach(function(element) {

			if(element.isOut && element.boundingBoxMode === 'inside' && !element.__editorMode) {

				FPDUtil.showMessage(
					element.title+': '+instance.getTranslation('misc', 'out_of_bounding_box')
				);

				product = false;
			}

		});

		//abort process
		if(product === false) {
			return false;
		}

		//add views
		for(var i=0; i < instance.viewInstances.length; ++i) {

			var viewInstance = instance.viewInstances[i],
				relevantViewOpts = viewInstance.getOptions();

			var viewElements = instance.viewInstances[i].stage.getObjects(),
				jsonViewElements = [];

			for(var j=0; j < viewElements.length; ++j) {

				var element = viewElements[j];

				if(element.title !== undefined && element.source !== undefined) {

					var jsonItem = {
						title: element.title,
						source: element.source,
						parameters: instance.viewInstances[i].getElementJSON(element),
						type: FPDUtil.getType(element.type)
					};

					if(relevantViewOpts.printingBox && relevantViewOpts.printingBox.hasOwnProperty('left')  && relevantViewOpts.printingBox.hasOwnProperty('top')) {
						var pointLeftTop = element.getPointByOrigin('left', 'top'),
							bbTL = new fabric.Point(relevantViewOpts.printingBox.left, relevantViewOpts.printingBox.top),
							bbBR = new fabric.Point(relevantViewOpts.printingBox.left + relevantViewOpts.printingBox.width, relevantViewOpts.printingBox.top  + relevantViewOpts.printingBox.height)

						jsonItem.printingBoxCoords = {
							left: pointLeftTop.x - relevantViewOpts.printingBox.left,
							top: pointLeftTop.y - relevantViewOpts.printingBox.top,
							//visible: element.intersectsWithRect(bbTL, bbBR) || element.isContainedWithinRect(bbTL, bbBR)
						};

					}

					if(onlyEditableElements) {
						if(element.isEditable) {
							jsonViewElements.push(jsonItem);
						}
					}
					else {
						jsonViewElements.push(jsonItem);
					}
				}
			}

			var viewObj = {
				title: viewInstance.title,
				thumbnail: viewInstance.thumbnail,
				elements: jsonViewElements,
				options: relevantViewOpts,
				names_numbers: viewInstance.names_numbers,
				mask: viewInstance.mask,
				locked: viewInstance.locked
			};

			if(i == 0 && instance.currentViews[0].hasOwnProperty('productTitle')) {
				viewObj.productTitle = instance.currentViews[0].productTitle;
			}

			product.push(viewObj);

		}

		//returns an array with all views
		return product;

	};

	/**
	 * Get the translation of a label.
	 *
	 * @method getTranslation
	 * @param {String} section The section key you want - toolbar, actions, modules or misc.
	 * @param {String} label The label key.
	 */
	this.getTranslation = function(section, label, defaulText) {

		defaulText = defaulText === undefined ? '' : defaulText;

		if(instance.langJson) {

			section = instance.langJson[section];

			if(section) {
				return section[label] ? section[label] : defaulText;
			}

		}

		return defaulText;

	};

	/**
	 * Returns an array with all custom added elements.
	 *
	 * @method getCustomElements
	 * @param {string} [type='all'] The type of elements. Possible values: 'all', 'image', 'text'.
	 * @param {Number} [viewIndex=-1] The index of the target view. By default all views are target.
	 * @param {String} [deselectElement=true] Deselect current selected element.
	 * @return {array} An array with objects with the fabric object and the view index.
	 */
	this.getCustomElements = function(type, viewIndex, deselectElement) {

		var elements = this.getElements(viewIndex, type, deselectElement),
			customElements = [];

		elements.forEach(function(element) {

			if(element.isCustom) {

				var viewIndex = instance.$productStage.children('.fpd-view-stage').index(element.canvas.wrapperEl);
				customElements.push({element: element, viewIndex: viewIndex});

			}

		});

		return customElements;

	};

	/**
	 * Returns an array with all fixed elements.
	 *
	 * @method getFixedElements
	 * @param {string} [type='all'] The type of elements. Possible values: 'all', 'image', 'text'.
	 * @param {Number} [viewIndex=-1] The index of the target view. By default all views are target.
	 * @param {String} [deselectElement=true] Deselect current selected element.
	 * @return {array} An array with objects with the fabric object and the view index.
	 */
	this.getFixedElements = function(type, viewIndex, deselectElement) {

		var elements = this.getElements(viewIndex, type, deselectElement),
			fixedElements = [];

		elements.forEach(function(element) {

			if(element.fixed) {

				var viewIndex = instance.$productStage.children('.fpd-view-stage').index(element.canvas.wrapperEl);
				fixedElements.push({element: element, viewIndex: viewIndex});

			}

		});

		return fixedElements;

	};

	/**
	 * Adds a new custom image to the product stage. This method should be used if you are using an own image uploader for the product designer. The customImageParameters option will be applied on the images that are added via this method.
	 *
	 * @method addCustomImage
	 * @param {string} source The URL of the image.
	 * @param {string} title The title for the design.
	 * @param {Object} options Additional options.
	 * @param {number} [viewIndex] The index of the view where the element needs to be added to. If no index is set, it will be added to current showing view.
	 */
	this.addCustomImage = function(source, title, options, viewIndex) {

		options = options === undefined ? {} : options;
		viewIndex = viewIndex === undefined ? instance.currentViewIndex : viewIndex;

		var image = new Image;

		image.crossOrigin = "anonymous";
    	image.src = source;

    	this.toggleSpinner(true, instance.getTranslation('misc', 'loading_image'));
    	instance.$viewSelectionWrapper.addClass('fpd-disabled');

		image.onload = function() {

			instance._loadingCustomImage = false;

			var imageH = this.height,
				imageW = this.width,
				currentCustomImageParameters = instance.currentViewInstance.options.customImageParameters,
				imageParts = this.src.split('.');

			if(!FPDUtil.checkImageDimensions(instance, imageW, imageH)) {
				instance.toggleSpinner(false);
    			return false;
			}

			var fixedParams = {
				isCustom: true,
			};

			//enable color wheel for svg and when no colors are set
			if($.inArray('svg', imageParts) != -1 && !currentCustomImageParameters.colors) {
				fixedParams.colors = true;
			}


			instance.addElement(
    			'image',
    			source,
    			title,
	    		$.extend({}, currentCustomImageParameters, fixedParams, options),
	    		viewIndex
    		);

    		instance.$viewSelectionWrapper.removeClass('fpd-disabled');

		}

		image.onerror = function(evt) {
			FPDUtil.showModal('Image could not be loaded!');
		}

	};

	/**
	 * Sets the dimensions of all views.
	 *
	 * @method setDimensions
	 * @param {Number} width The width in pixel.
	 * @param {Number} height The height in pixel.
	 * @param {Number} [viewIndex=-1] The target views. -1 targets all views.
	 */
	this.setDimensions = function(width, height, viewIndex) {

		viewIndex = viewIndex === undefined ? -1 : viewIndex;

		var targetViews = [];
		if(viewIndex == -1) {

			instance.mainOptions.stageWidth = width;
			instance.mainOptions.stageHeight = height;
			targetViews = instance.viewInstances;

		}
		else {
			targetViews.push(instance.viewInstances[viewIndex]);
		}

		if(viewIndex == instance.currentViewIndex || viewIndex == -1) {
			instance.$container.find('.fpd-product-stage').width(width);
		}

		targetViews.forEach(function(targetView) {

			targetView.options.stageWidth = width;
			targetView.options.stageHeight = height;

			if(viewIndex == instance.currentViewIndex) {
				targetView.resetCanvasSize();
			}

		})

		if((viewIndex == instance.currentViewIndex || viewIndex == -1) && instance.mainBar && instance.mainBar.$content && instance.$container.filter('[class*="fpd-off-canvas-"]').length > 0) {

			instance.mainBar.$content.height(instance.$mainWrapper.height());
		}

	};

	/**
	 * Sets the order quantity.
	 *
	 * @method setOrderQuantity
	 * @param {Number} quantity The width in pixel.
	 */
	this.setOrderQuantity = function(quantity) {

		quantity = quantity == '' || quantity < 0 ? 1 : quantity;
		instance.orderQuantity = quantity;

		var truePrice = instance.calculatePrice();

		$elem.trigger('priceChange', [null, truePrice, instance.singleProductPrice]);

	};

	/**
	 * Returns an order object containing the product from the getProduct() method. If using plus add-on and bulk variations, the variations will be added into the object.
	 *
	 * @method getOrder
	 * @param {Object} [options={}] Options for the methods that are called inside this mehtod, e.g. getProduct() can receive two parameters.
	 * @return {object} An object containing different objects representing important order data.
	 * @example
	 * // includes only editable elements and the user needs to customize the initial product
	 * fpd.getOrder( {onlyEditableElements: true, customizationRequired: true} );
	 */
	this.getOrder = function(options) {

		options = options === undefined ? {} : options;

		instance._order.product = instance.getProduct(
			options.onlyEditableElements || false,
			options.customizationRequired || false
		);

		instance._order.usedFonts = instance.getUsedFonts();
		instance._order.usedColors = [];

		instance.getUsedColors().forEach(function(hexValue) {

			var colorName = instance.mainOptions.hexNames[hexValue.replace('#', '').toLowerCase()],
				colorItem = {hex: hexValue};

			if(colorName) {
				colorItem.name = colorName;
			}

			instance._order.usedColors.push(colorItem)
		});

		instance._order.usedDepositPhotos = instance.getDepositPhotos();

		$elem.trigger('getOrder');

		return instance._order;

	};

	/**
	 * Get all fonts used in the product.
	 *
	 * @method getUsedFonts
	 * @return {array} An array with objects containing the font name and optional the URL to the font.
	 */
	this.getUsedFonts = function() {

		var _usedFonts = [], //temp to check if already included
			usedFonts = [];

		this.getElements(-1, 'all', false).forEach(function(element) {

			if(FPDUtil.getType(element.type) === 'text') {

				if(_usedFonts.indexOf(element.fontFamily) === -1) {

					var fontObj = {name: element.fontFamily},
						//grep font entry
						result = $.grep(instance.mainOptions.fonts, function(e){
							return e.name == element.fontFamily;
						});

					//check if result contains props and url prop
					if(result.length > 0 && result[0].url) {
						fontObj.url = result[0].url;
					}

					_usedFonts.push(element.fontFamily);
					usedFonts.push(fontObj);


				}

			}

		});

		return usedFonts;

	};

	/**
	 * Get all used colors from a single or all views.
	 *
	 * @method getUsedColors
	 * @param {Number} [viewIndex=-1] The index of the target view. By default all views are target.
	 * @return {array} An array with hexdecimal color values.
	 */
	this.getUsedColors = function(viewIndex) {

		var usedColors = [];
		this.getElements(viewIndex, 'all', false).forEach(function(element) {

			var type = FPDUtil.elementIsColorizable(element);
			if(type) {

				if(type === 'svg') {

					if(element.type === FPDPathGroupName) { //multi pathes
						element.getObjects().forEach(function(path) {
							if(FPDUtil.isHex(path.fill)) {
								usedColors.push(path.fill);
							}
						});
					}
					else { //single path
						if(FPDUtil.isHex(element.fill)) {
							usedColors.push(element.fill);
						}
					}

				}
				else {
					if(FPDUtil.isHex(element.fill)) {
						usedColors.push(element.fill);
					}
				}
			}

		});

		return FPDUtil.arrayUnique(usedColors);

	};


	/**
	 * Calculates the total price considering the elements price in all views and pricing rules.
	 *
	 * @method calculatePrice
	 * @param {Boolean} [considerQuantity=true] Calculate with or without quantity.
	 * @return {Number} The calculated price.
	 */
	this.calculatePrice = function(considerQuantity) {

		considerQuantity = considerQuantity === undefined ? true : considerQuantity;

		_calculateViewsPrice();

		var calculatedPrice = instance.singleProductPrice;
		instance.currentPrice = calculatedPrice;

		calculatedPrice += instance.pricingRulesPrice;

		if(considerQuantity) {
			calculatedPrice *= instance.orderQuantity;
		}

		//price has decimals, set max. decimals to 2
		if(calculatedPrice % 1 != 0) {
			calculatedPrice = Number(calculatedPrice.toFixed(2));
		}

		return calculatedPrice;

	}

	/**
	 * Removes a view by its index value.
	 *
	 * @method removeView
	 * @param {Number} [viewIndex=0] The index of the target view.
	 */
	this.removeView = function(viewIndex) {

		viewIndex = viewIndex === undefined ? 0 : viewIndex;

		var $viewStage = instance.$productStage.children('.fpd-view-stage').eq(viewIndex);

		instance.$viewSelectionWrapper.find('.fpd-item').eq(viewIndex).remove();
		$viewStage.remove();

		instance.viewInstances.splice(viewIndex, 1);

		//select next view if removing view is showing
		if(instance.viewInstances.length > 0) {
			viewIndex == instance.currentViewIndex ? instance.selectView(0) : instance.selectView(viewIndex);
		}

		/**
		 * Gets fired when a view is removed.
		 *
		 * @event FancyProductDesigner#viewRemove
		 * @param {Event} event
		 * @param {Number} viewIndex
		 */
		$elem.trigger('viewRemove', [viewIndex]);

		var truePrice = instance.calculatePrice();

		/**
	     * Gets fired as soon as the price changes in a view.
	     *
	     * @event FancyProductDesigner#priceChange
	     * @param {Event} event
	     * @param {number} elementPrice - The price of the element.
	     * @param {number} totalPrice - The true price of all views with quantity.
	     * @param {number} singleProductPrice - The true price of all views without quantity.
	     */
		$elem.trigger('priceChange', [null, truePrice, instance.singleProductPrice]);

	};

	/**
	 * Formats the price to a string with the currency and the decimal as well as the thousand separator.
	 *
	 * @method formatPrice
	 * @param {Number} [price] The price thats gonna be formatted.
	 * @return {String} The formatted price string.
	 */
	this.formatPrice = function(price) {

		if(typeof instance.mainOptions.priceFormat === 'object') {

			var thousandSep = instance.mainOptions.priceFormat.thousandSep,
				decimalSep = instance.mainOptions.priceFormat.decimalSep;

			var splitPrice = price.toString().split('.'),
				absPrice = splitPrice[0],
				decimalPrice = splitPrice[1],
				tempAbsPrice = '';

			if (typeof absPrice != 'undefined') {

				for (var i=absPrice.length-1; i>=0; i--) {
					tempAbsPrice += absPrice.charAt(i);
				}

				tempAbsPrice = tempAbsPrice.replace(/(\d{3})/g, "$1" + thousandSep);
				if (tempAbsPrice.slice(-thousandSep.length) == thousandSep) {
					tempAbsPrice = tempAbsPrice.slice(0, -thousandSep.length);
				}

				absPrice = '';
				for (var i=tempAbsPrice.length-1; i>=0 ;i--) {
					absPrice += tempAbsPrice.charAt(i);
				}

				if (typeof decimalPrice != 'undefined' && decimalPrice.length > 0) {
					//if only one decimal digit add zero at end
					if(decimalPrice.length == 1) {
						decimalPrice += '0';
					}
					absPrice += decimalSep + decimalPrice;
				}

			}

			absPrice = instance.mainOptions.priceFormat.currency.replace('%d', absPrice.toString());

			return absPrice;

		}
		else {
			price = instance.mainOptions.priceFormat.replace('%d', price);
		}

		return price;

	};

	//translates a HTML element
	this.translateElement = function($tag) {

		var label = '';
		if(instance.langJson) {

			var objString = '';

			if($tag.attr('placeholder') !== undefined) {
				objString = $tag.attr('placeholder');
			}
			else if($tag.attr('title') !== undefined) {
				objString = $tag.attr('title');
			}
			else if($tag.data('title') !== undefined) {
				objString = $tag.data('title');
			}
			else {
				objString = $tag.text();
			}

			var keys = objString.split('.'),
				firstObject = instance.langJson[keys[0]];

			if(firstObject) { //check if object exists

				label = firstObject[keys[1]];

				if(label === undefined) { //if label does not exist in JSON, take default text
					label = $tag.data('defaulttext');
				}

			}
			else {
				label = $tag.data('defaulttext');
			}

			//store all translatable labels in json
			var sectionObj = instance.languageJSON[keys[0]];
			sectionObj[keys[1]] = label;

		}
		else {
			label = $tag.data('defaulttext');
		}

		if($tag.attr('placeholder') !== undefined) {
			$tag.attr('placeholder', label).text('');
		}
		else if($tag.attr('title') !== undefined) {
			$tag.attr('title', label);
		}
		else if($tag.data('title') !== undefined) {
			$tag.data('title', label);
		}
		else {
			$tag.text(label);
		}

		return label;

	};

	this.selectGuidedTourStep = function(target) {

		$body.children('.fpd-gt-step').remove();

		var keyIndex = Object.keys(instance.mainOptions.guidedTour).indexOf(target),
			splitTarget = target.split(':'),
			$targetElem = null;

		if(splitTarget[0] === 'module') {
			$targetElem = $mainBar.find('.fpd-navigation').children('[data-module="'+splitTarget[1]+'"]');
		}
		else if(splitTarget[0] === 'action') {
			$targetElem = $('.fpd-action-btn[data-action="'+splitTarget[1]+'"]');
		}
		else if(splitTarget.length === 1) { //css selector
			$targetElem = $(splitTarget[0]);
		}

		if($targetElem) {

			//if module or action is not available, go to next
			if($targetElem.length === 0) {

				if(Object.keys(instance.mainOptions.guidedTour)[keyIndex+1]) {
					instance.selectGuidedTourStep(Object.keys(instance.mainOptions.guidedTour)[keyIndex+1]);
				}

				return;
			}

			var $step = $body.append('<div class="fpd-container fpd-gt-step"><div class="fpd-gt-pointer"><span class="fpd-icon-arrow-dropdown"></span></div><div class="fpd-gt-close"><span class="fpd-icon-close"></span></div><div class="fpd-gt-text">'+instance.mainOptions.guidedTour[target]+'</div><div class="fpd-gt-actions fpd-clearfix"><div class="fpd-gt-next fpd-btn fpd-primary">'+instance.getTranslation('misc', 'guided_tour_next')+'</div><div class="fpd-gt-back fpd-btn fpd-primary">'+instance.getTranslation('misc', 'guided_tour_back')+'</div><span class="fpd-gt-counter">'+String(keyIndex +1)+'/'+Object.keys(instance.mainOptions.guidedTour).length+'</span></div></div>').children('.fpd-gt-step'),
				targetPos = $targetElem.offset(),
				offsetX = $targetElem.outerWidth() * 0.5,
				offsetY = 0,
				stepLeft = targetPos.left + offsetX;

			if(stepLeft < 24) {
				stepLeft = 24;
			}

			//position step
			$step.css({
				left: stepLeft,
				top: targetPos.top + $targetElem.outerHeight() + offsetY,
			});

			//if step is outside viewport, reposition step and pointer
			if($step.outerWidth() + stepLeft > window.innerWidth) {
				offsetX = (window.innerWidth - ($step.outerWidth() + stepLeft));
				$step.css('left', stepLeft + offsetX)
				.children('.fpd-gt-pointer').css('margin-left', Math.abs(offsetX));
			}

			//set back btn
			if(Object.keys(instance.mainOptions.guidedTour)[keyIndex-1]) {
				$step.find('.fpd-gt-back').data('target', Object.keys(instance.mainOptions.guidedTour)[keyIndex-1]);
			}
			else {
				$step.find('.fpd-gt-back').hide();
			}

			//set next btn
			if(Object.keys(instance.mainOptions.guidedTour)[keyIndex+1]) {
				$step.find('.fpd-gt-next').data('target', Object.keys(instance.mainOptions.guidedTour)[keyIndex+1]);
			}
			else {
				$step.find('.fpd-gt-next').hide();
			}

		}

	};

	/**
	 * Set up the products with a JSON.
	 *
	 * @method setupProducts
	 * @param {Array} products An array containg the products or categories with products.
	 * @example [{
	 "category": "Category Title", "products":
	 	[{"productTitle": "TITLE OF PRODUCT", "productThumbnail": "THUMBNAIL OF PRODUCT" "title": "TITLE OF VIEW", "thumbnail": "THUMBNAIL OF VIEW", "OPTIONS": {OBJECT VIEW OPTIONS}, "ELEMENTS": [ARRAY OF ELEMENTS]
	 	...
]}
	 */
	this.setupProducts = function(products) {

		products = products === undefined ? [] : products;

		this.products = [];

		products.forEach(function(productItem) {

			if(productItem.hasOwnProperty('category')) { //check if products JSON contains categories

				productItem.products.forEach(function(singleProduct) {
					instance.addProduct(singleProduct, productItem.category);
				});

			}
			else {
				instance.addProduct(productItem);
			}

		});

		//load first product
		if(instance.mainOptions.loadFirstProductInStage && products.length > 0 && !stageCleared) {
			instance.selectProduct(0);
		}
		else {
			instance.toggleSpinner(false);
		}

		/**
	     * Gets fired as soon as products are set either from the HTML or added as JSON.
	     *
	     * @event FancyProductDesigner#productsSet
	     * @param {Event} event
	     * @param {Array} products - An array containing the products.
	     */
		$elem.trigger('productsSet', [instance.products]);

	};

	/**
	 * Set up the designs with a JSON.
	 *
	 * @method setupDesigns
	 * @param {Array} designs An array containg the categories with designs.
	 * @example [{
	 "title": "Category Title", "thumbnail": "Thumbnail of Category", "designs": [ARRAY OF ELEMENTS]},
	 {"title": "Category Title", "thumbnail": "Thumbnail of Category", "category": [
	 		{"title": "Category Child", "thumbnail": "Thumbnail of Category", "designs": [ARRAY OF ELEMENTS]}
	 ]}
]
	 */
	this.setupDesigns = function(designs) {

		instance.designs = designs;

		/**
	     * Gets fired as soon as designs are set either from the HTML or added as JSON.
	     *
	     * @event FancyProductDesigner#designsSet
	     * @param {Event} event
	     * @param {Array} designs - An array containing the designs.
	     */
		$elem.trigger('designsSet', [instance.designs]);

	};

	/**
	 * Toggle the responsive behavior.
	 *
	 * @method toggleResponsive
	 * @param {Boolean} [toggle] True or false.
	 * @return {Boolean} Returns true or false.
	 */
	this.toggleResponsive = function(toggle) {

		toggle = toggle === undefined ? $elem.hasClass('fpd-not-responsive') : toggle;

		$elem.toggleClass('fpd-not-responsive', !toggle);
		this.viewInstances.forEach(function(viewInstance, viewIndex) {

			viewInstance.options.responsive = toggle;

			if(viewIndex == instance.currentViewIndex) {
				viewInstance.resetCanvasSize();
			}

		});

		return toggle;

	};

	/**
	 * Get an array with image objects from depositphotos.com that are used in the current showing product.
	 *
	 * @method getDepositPhotos
	 * @return {Array} An array containing objects. The object contains the depositphotos media ID and the URL to the image that has been uploaded to the server.
	 */
	this.getDepositPhotos = function() {

		var dpImages = [];
		this.getElements(-1, 'image').forEach(function(imgItem) {

			if(imgItem.source && imgItem.depositphotos) {
				dpImages.push({depositphotos: imgItem.depositphotos, source: imgItem.source});
			}

		});

		return dpImages;

	};

	/**
	 * Get an object containing the download link for a media. Whenever you call this method, credits will be taken from your depositphotos account to purchase a media. You can find more infos about the Depositphotos API here: http://api.depositphotos.com/doc/classes/API.Purchase.html
	 *
	 * @method getDepositPhotosPurchaseMedia
	 * @param {Function} callback A function that will be called with JSON data has been loaded. Will also be executed on failure.
	 * @param {String} mediaID A Depositphotos media ID.
	 * @param {String} username Your Depositphotos username.
	 * @param {String} password Your Depositphotos password.
	 * @param {String} [size=s] Possible values: s/m/l/xl/vect/el0.
	 * @param {String} [license=standard] Possible values: standard or extended.
	 * @param {String} [purchaseCurrency=credits] The license. Possible values: 'credits' | 'subscription' | 'bonus' | 'ondemand'.
	 * @example
fpd.getDepositPhotosPurchaseMedia(function(data) {
}, '12345', 'username', 'password', 'm')
	 */
	this.getDepositPhotosPurchaseMedia = function(callback, mediaID, username, password, size, license, purchaseCurrency) {

		size = size === undefined ? 's' : size;
		license = license === undefined ? 'standard' : license;
		purchaseCurrency = purchaseCurrency === undefined ? 'credits' : purchaseCurrency;

		var loginObj = {
			dp_apikey: instance.mainOptions.depositphotosApiKey,
			dp_command: 'login',
			dp_login_user: username,
			dp_login_password: password
		};

		$.getJSON(location.protocol+'//api.depositphotos.com?'+$.param(loginObj), function(loginData) {

			if(loginData.error) {

				callback(loginData);
				alert(loginData.error.errormsg);

			}
			else if(loginData.sessionid) {

				var mediaObj = {
					dp_apikey: instance.mainOptions.depositphotosApiKey,
					dp_command: 'getMedia',
					dp_session_id: loginData.sessionid,
					dp_media_id: mediaID,
					dp_media_option: size,
					dp_media_license: license,
					dp_purchase_currency: purchaseCurrency,
					dp_force_purchase_method: purchaseCurrency
				};

				$.getJSON(location.protocol+'//api.depositphotos.com?'+$.param(mediaObj), function(mediaData) {

					callback(mediaData);

					if(mediaData.error) {
						alert(mediaData.error.errormsg);
					}
					else {

					}

				})

			}
			else {

				callback(loginData);
				alert("No Sessions ID!");

			}

		});


	};

	/**
	 * Load custom fonts or from Google webfonts  used in the product designer.
	 *
	 * @method loadFonts
	 * @param {Array} fonts An array containing objects with name and URL to the font file.
	 * @param {Function} callback A function that will be called when all fonts have been loaded.
	 * @version 4.7.6
	 */
	this.loadFonts = function(fonts, callback) {

		if(fonts && fonts.length > 0 && typeof fonts[0] === 'object') {

			var googleFonts = [],
				customFonts = [],
				fontStateCount = 0,
				$customFontsStyle;

			if(instance.$container.prevAll('#fpd-custom-fonts').length == 0) {

				$customFontsStyle = $('<style type="text/css" id="fpd-custom-fonts"></style>');
				instance.$container.before($customFontsStyle);

			}
			else {
				$customFontsStyle = instance.$container.prevAll('#fpd-custom-fonts:first').empty();
			}

			fonts.forEach(function(fontItem) {

				if(fontItem.hasOwnProperty('url')) {

					if(fontItem.url == 'google') { //from google fonts
						googleFonts.push(fontItem.name+':400,400i,700,700i');
					}
					else { //custom fonts

						var fontFamily = fontItem.name;

						fontFamily += ':n4'

						if(fontItem.variants) {
							fontFamily += ','+Object.keys(fontItem.variants).toString();
						}

						customFonts.push(fontFamily);

						$customFontsStyle.append(FPDUtil.parseFontsToEmbed(fontItem, instance.mainOptions._loadFromScript));

					}

				}

			});

			var _fontActiveState = function(state, familyName, fvd) {

				if(state == 'inactive') {
					FPDUtil.log(familyName+' font could not be loaded.', 'warn');
				}

				if(fontStateCount == (googleFonts.length + customFonts.length)-1) {
					callback();
				}

				fontStateCount++;

			};

			var WebFontOpts = {
				 fontactive: function(familyName, fvd) {
				    _fontActiveState('active', familyName, fvd);
			    },
			    fontinactive: function(familyName, fvd) {
				    _fontActiveState('inactive', familyName, fvd);
				},
			    timeout: 3000
			};

			if(googleFonts.length > 0) {
				WebFontOpts.google = {families: googleFonts};
			}

			if(customFonts.length > 0) {
				WebFontOpts.custom = {families: customFonts};
			}

			if(typeof WebFont !== 'undefined' && (googleFonts.length > 0 || customFonts.length > 0)) {
				WebFont.load(WebFontOpts);
			}
			else {
				callback();
			}


		}
		else {
			callback();
		}

	};

	/**
	 * Generates an object that will be used for the print-ready export. This objects includes the used fonts and the SVG data strings to generate the PDF.
	 *
	 * @method getPrintOrderData
	 * @version 4.7.6
	 */
	this.getPrintOrderData = function() {

		var printOrderData = {
			used_fonts: instance.getUsedFonts(),
			svg_data: [],
			custom_images: []
		};

		for(var i=0; i < instance.viewInstances.length; ++i) {

			printOrderData.svg_data.push({
				svg: instance.viewInstances[i].toSVG({}, null, true),
				output: instance.viewInstances[i].options.output
			});

		}

		instance.getCustomElements('image').forEach(function(img) {

			printOrderData.custom_images.push(img.element.source);

		})

		return printOrderData;

	};

	_initialize();

};