Home Reference Source

src/components/sketchpad/Sketchpad.js

import Field from '../../components/_classes/field/Field';
import Two from 'two.js/src/two';
import Picker from 'vanilla-picker';
import _ from 'lodash';
import Formio from '../../Formio';
import editForm from './Sketchpad.form';
import NativePromise from 'native-promise-only';

export default class Sketchpad extends Field {
  static schema(...extend) {
    return Field.schema({
      type: 'sketchpad',
      label: 'Sketchpad',
      key: 'sketchpad',
      defaultZoom: 100
    }, ...extend);
  }

  static builderInfo = {
    title: 'Sketchpad',
    group: 'premium',
    icon: 'pencil',
    weight: 110,
    documentation: 'http://help.form.io/userguide/',
    schema: Sketchpad.schema()
  };

  static editForm = editForm;

  constructor(...args) {
    super(...args);
    _.defaults(this.component, {
      defaultZoom: 100
    });
    this.deleted = [];
    this.viewSketchpad = {
      canvas: {},
      background: {}
    };
    this.editSketchpad = {
      canvas: {},
      background: {}
    };
    //will use dimensions from background viewBox if either width or height is not defined for component
    //TODO maybe change this criteria to AND instead of OR, use defined dimension and default another missing dimension to value from viewBox (in this case will need to use promise in case of any missing dimension
    this.useBackgroundDimensions = !this.component.width || !this.component.height;
    //initialize backgroundReady promise
    const backgroundReadyPromise = new NativePromise((resolve, reject) => {
      this.backgroundReady = {
        resolve,
        reject
      };
    });
    this.backgroundReady.promise = backgroundReadyPromise;
    //default state of SVG editor
    this.state = {
      mode: Object.keys(this.modes)[0],
      stroke: '#333',
      fill: '#ccc',
      linewidth: 1,
      circleSize: 10
    };

    this.dimensionsMultiplier = 1;

    this.zoomInfo = {
      viewBox: {},
      multiplier: 1.5,
      totalMultiplier: 1
    };
  }

  /**
   * Builds the component.
   */
  render() {
    return super.render('coming soon', {});
    // return super.render(this.renderTemplate('sketchpad', {}));
  }

  getValue() {
    return this.dataValue;
  }

  get emptyValue() {
    return [];
  }

  get modes() {
    return {
      pencil: {
        icon: 'pencil',
        title: 'Pencil',
        state: {
          mode: 'pencil'
        },
        eventStart: (coordinate) => {
          this.points = [coordinate];
          this.prev = coordinate;
          this.curve = this.two.makeCurve([
            new Two.Vector(this.prev.x, this.prev.y),
            new Two.Vector(coordinate.x, coordinate.y + 1)
          ], true);
          this.curve.noFill().stroke = this.state.stroke;
          this.curve.linewidth = this.state.linewidth;
          this.curve.vertices.forEach((v) => v.addSelf(this.curve.translation));
          this.curve.translation.clear();
          this.two.update();
          this.layers.push(this.curve);
          this.curve._renderer.elem.addEventListener('click', (e) => this.click(e, this.layers.length));
        },
        drag: (coordinate) => {
          this.points.push(coordinate);
          this.curve.vertices.push(new Two.Vector(coordinate.x, coordinate.y));
          this.two.update();
          this.prev = coordinate;
        },
        eventEnd: () => {
          const value = this.editValue.slice();
          value.push(Object.assign({}, this.state, { points: this.points }));
          this.editValue = value;
          this.triggerChange();
        },
        draw: (state) => {
          const layer = this.two.makeCurve(state.points.map(point => new Two.Vector(point.x, point.y)), true);
          layer.noFill().stroke = state.stroke;
          layer.linewidth = state.linewidth;
          layer.vertices.forEach((v) => v.addSelf(layer.translation));
          layer.translation.clear();
          return layer;
        }
      },
      line: {
        icon: 'minus',
        title: 'Line',
        state: {
          mode: 'line'
        },
        eventStart: (coordinate) => {
          this.center = coordinate;
          this.line = this.two.makeLine(
            coordinate.x,
            coordinate.y,
            coordinate.x,
            coordinate.y
          );
          this.line.fill = this.state.fill;
          this.line.stroke = this.state.stroke;
          this.line.linewidth = this.state.linewidth;
          this.two.update();
          this.layers.push(this.line);
          const index = this.layers.length - 1;
          this.line._renderer.elem.addEventListener('click', (e) => this.click(e, index));
        },
        drag: (coordinate) => {
          this.line.vertices[1].x = coordinate.x;
          this.line.vertices[1].y = coordinate.y;
          this.two.update();
        },
        eventEnd: () => {
          const value = this.editValue.slice();
          const vertices = this.line.vertices.map(vertice => {
            return {
              x: vertice.x,
              y: vertice.y
            };
          });
          value.push(Object.assign({}, this.state, { vertices: vertices }));
          this.editValue = value;
          this.triggerChange();
        },
        draw: (state) => {
          const layer = this.two.makeLine(
            state.vertices[0].x,
            state.vertices[0].y,
            state.vertices[1].x,
            state.vertices[1].y
          );
          layer.fill = state.fill;
          layer.stroke = state.stroke;
          layer.linewidth = state.linewidth;
          return layer;
        }
      },
      circle: {
        icon: 'circle',
        title: 'Circle',
        state: {
          mode: 'circle'
        },
        eventStart: (coordinate) => {
          this.center = coordinate;
          const layer = this.two.makeCircle(coordinate.x, coordinate.y, this.state.circleSize);
          layer.fill = this.state.fill;
          layer.stroke = this.state.stroke;
          layer.linewidth = this.state.linewidth;
          this.two.update();
          this.layers.push(layer);
          const index = this.layers.length - 1;
          layer._renderer.elem.addEventListener('click', (e) => this.click(e, index));
        },
        drag: () => {

        },
        eventEnd: () => {
          const value = this.editValue.slice();
          value.push(Object.assign({}, this.state, { center: this.center }));
          this.editValue = value;
          this.triggerChange();
        },
        draw: (state) => {
          const layer = this.two.makeCircle(state.center.x, state.center.y, state.circleSize);
          layer.fill = state.fill;
          layer.stroke = state.stroke;
          layer.linewidth = state.linewidth;
          return layer;
        },
        attach: (element) => {
          const radiusInput = this.ce('input', {
            type: 'number',
            class: 'formio-sketchpad-toolbar-input formio-sketchpad-radius-input',
            onChange: (e) => {
              this.state.circleSize = e.target.value;
            }
          });
          radiusInput.value = this.state.circleSize;
          element.appendChild(radiusInput);
          return element;
        }
      },
      rectangle: {
        icon: 'square-o',
        cursor: {
          hover: 'crosshair'
        },
        title: 'Rectangle',
        state: {
          mode: 'rectangle'
        },
        eventStart: (coordinate) => {
          this.dragStartPoint = coordinate;
        },
        drag: (coordinate) => {
          this.dragEndPoint = coordinate;
          if (this.rectangle) {
            this.rectangle.remove();
          }
          this.width = Math.abs(this.dragEndPoint.x - this.dragStartPoint.x);
          this.height = Math.abs(this.dragEndPoint.y - this.dragStartPoint.y);
          this.center = {
            x: Math.min(this.dragStartPoint.x, this.dragEndPoint.x) + this.width / 2,
            y: Math.min(this.dragStartPoint.y, this.dragEndPoint.y) + this.height / 2
          };
          this.rectangle = this.two.makeRectangle(this.center.x, this.center.y, this.width, this.height);
          this.rectangle.fill = this.state.fill;
          this.rectangle.stroke = this.state.stroke;
          this.rectangle.linewidth = this.state.linewidth;
          this.two.update();
          this.layers.push(this.rectangle);
          const index = this.layers.length - 1;
          this.rectangle._renderer.elem.addEventListener('click', (e) => this.click(e, index));
        },
        eventEnd: () => {
          const value = this.editValue.slice();
          delete this.rectangle;
          const rectangleState = {
            center: this.center,
            width: this.width,
            height: this.height
          };
          value.push(Object.assign({}, this.state, rectangleState));
          this.editValue = value;
          this.triggerChange();
        },
        draw: (state) => {
          const layer = this.two.makeRectangle(state.center.x, state.center.y, state.width, state.height);
          layer.fill = state.fill;
          layer.stroke = state.stroke;
          layer.linewidth = state.linewidth;
          return layer;
        },
      },
      zoomIn: {
        icon: 'search-plus',
        cursor: {
          hover: 'zoom-in'
        },
        title: 'Zoom In',
        state: {
          mode: 'zoomIn'
        },
        eventStart: (coordinate) => {
          this.zoom(coordinate, this.zoomInfo.multiplier);
        }
      },
      zoomOut: {
        icon: 'search-minus',
        cursor: {
          hover: 'zoom-out'
        },
        title: 'Zoom Out',
        state: {
          mode: 'zoomOut'
        },
        eventStart: (coordinate) => {
          this.zoom(coordinate, 1 / this.zoomInfo.multiplier);
        }
      },
      drag: {
        icon: 'hand-paper-o',
        title: 'Drag Zoomed Image',
        cursor: {
          hover: 'grab',
          clicked: 'grabbing'
        },
        state: {
          mode: 'drag'
        },
        eventStart: (coordinate) => {
          this.dragStartPoint = coordinate;
        },
        drag: (coordinate) => {
          if (!this.dragLastPoint) {
            this.dragLastPoint = this.dragStartPoint;
          }
          const offset = {
            x: Math.round(coordinate.x - this.dragStartPoint.x),
            y: Math.round(coordinate.y - this.dragStartPoint.y)
          };
          if (offset.x !== 0 || offset.y !== 0) {
            this.dragImage(offset);
            this.dragLastPoint = coordinate;
          }
        }
      }
    };
  }

  get styles() {
    return [
      {
        icon: 'square-o',
        title: 'Stroke Color',
        type: 'colorpicker',
        property: 'stroke',
        attach: (element) => {
          const picker = new Picker(element);
          picker.setColor(this.state.stroke, true);
          picker.onChange = (color) => {
            this.state.stroke = color.rgbaString;
            element.style.color = color.rgbaString;
          };
          return element;
        }
      },
      {
        icon: 'square',
        title: 'Fill Color',
        type: 'colorpicker',
        property: 'fill',
        attach: (element) => {
          const picker = new Picker(element);
          picker.setColor(this.state.fill, true);
          picker.onChange = (color) => {
            this.state.fill = color.rgbaString;
            element.style.color = color.rgbaString;
          };
          return element;
        }
      },
      {
        icon: 'minus',
        title: 'Line Width',
        type: 'number',
        property: 'linewidth',
        attach: (element) => {
          const widthInput = this.ce('input', {
            type: 'number',
            class: 'formio-sketchpad-toolbar-input formio-sketchpad-linewidth-input',
            onChange: (e) => {
              this.state.linewidth = e.target.value;
            }
          });
          widthInput.value = this.state.linewidth;
          element.appendChild(widthInput);
          return element;
        }
      }
    ];
  }

  get actions() {
    return [
      {
        icon: 'undo',
        action: 'undo',
        title: 'Undo'
      },
      {
        icon: 'repeat',
        action: 'redo',
        title: 'Redo'
      },
      {
        icon: 'search',
        action: 'resetZoom',
        title: 'Reset Zoom'
      },
      {
        icon: 'ban',
        action: 'clearAll',
        title: 'Clear All'
      }
    ];
  }

  editSvg() {
    if (this.options.readOnly) {
      return;
    }
    //open editor in modal
    this.editorModal = this.createModal();
    this.addClass(this.editorModal, 'formio-sketchpad-edit-dialog');
    this.addClass(this.editorModal.body, 'formio-sketchpad-edit-dialog-body');
    const toolbar = this.createToolbar();
    const metaInfoContainer = this.ce('div', { class: 'formio-sketchpad-meta-info' },
      this.ce('span', {}, [
        this.totalMultiplierElement = this.ce('span', {}, this.t(Math.round(this.zoomInfo.totalMultiplier) * 100) / 100),
        this.t('x')
      ])
    );
    this.saveSvgButton = this.ce('button', {
      class: 'btn btn-success formio-sketchpad-save-button'
    }, this.t('Save'));
    this.addEventListener(this.saveSvgButton, 'click', () => {
      this.saveSvg();
      this.editorModal.close(true);
    });
    this.editorModalHeader = this.ce('div', { class: 'formio-sketchpad-edit-dialog-header' }, [toolbar]);
    this.editorModalFooter = this.ce('div', { class: 'formio-sketchpad-edit-dialog-footer' }, [metaInfoContainer, this.saveSvgButton]);
    this.editorModalContent = this.ce('div', {
      class: 'formio-edit-sketchpad-container'
    }, [
      this.editSketchpad.canvas.container,
      this.editSketchpad.background.container
    ]);
    this.editorModal.body.appendChild(this.editorModalHeader);
    this.editorModal.body.appendChild(this.editorModalContent);
    this.editorModal.body.appendChild(this.editorModalFooter);
    const resizeListener = () => {
      this.stretchDrawingArea();
      this.setEditorSize(this.dimensions.width, this.dimensions.height);
    };
    window.addEventListener('resize', resizeListener);
    this.stretchDrawingArea();
    this.editValue = _.cloneDeep(this.dataValue);
    this.draw(this.editValue);
    const initialDialogClose = this.editorModal.close;
    this.editorModal.close = (ignoreWarning) => {
      if (ignoreWarning || confirm('Are you sure you want to close? Your unsaved progress will be lost')) {
        this.resetZoom();
        window.removeEventListener('resize', resizeListener);
        initialDialogClose();
      }
    };
    this.resetZoom();
  }

  stretchDrawingArea() {
    const [modalWidth, modalHeight] = [this.editorModal.bodyContainer.clientWidth, this.editorModal.bodyContainer.clientHeight];
    const computedStyle = getComputedStyle(this.editorModal.bodyContainer);
    const [paddingTop, paddingBottom, paddingLeft, paddingRight] = ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'].map(property => {
      return parseFloat(computedStyle[property]);
    });
    const [headerHeight, footerHeight] = [this.editorModalHeader.offsetHeight, this.editorModalFooter.offsetHeight],
      //dimensions of available space in modal
      availableWidth = modalWidth - paddingLeft - paddingRight,
      availableHeight = modalHeight - paddingTop - paddingBottom - headerHeight - footerHeight,
      //default width of drawing area
      defaultWidth = this.zoomInfo.viewBox.default.width,
      defaultHeight = this.zoomInfo.viewBox.default.height,
      widthRatio = availableWidth / defaultWidth,
      heightRatio = availableHeight / defaultHeight;
    //use the smallest ratio as multiplier so that drawing area doesn't overflow popup in any dimension
    this.dimensionsMultiplier = Math.min(widthRatio, heightRatio);
    //calculate new dimensions so that drawing area fills all free modal space
    this.dimensions.width = Math.round(defaultWidth * this.dimensionsMultiplier);
    this.dimensions.height = Math.round(defaultHeight * this.dimensionsMultiplier);
  }

  saveSvg() {
    this.dataValue = this.editValue;
    this.copySvgToView();
  }

  createToolbar() {
    /* eslint-disable max-len */
    return this.ce('div', {
      class: 'btn-toolbar formio-sketchpad-toolbar',
      role: 'toolbar'
    }, [
      this.ce('div', {
          class: 'btn-group formio-sketchpad-toolbar-group',
          role: 'group'
        },
        this.modeButtons = Object.keys(this.modes).map(key => {
          const mode = this.modes[key];
          const toolbarButton = this.ce('div', {
            class: `btn btn-secondary formio-sketchpad-toolbar-button formio-sketchpad-toolbar-button-${key} ${this.state.mode === mode.state.mode ? ' active' : ''}`,
            onClick: () => this.setState(mode.state),
            title: mode.title
          }, this.ce('i', {
            class: `fa fa-${mode.icon}`,
          }));
          if (mode.attach) {
            return mode.attach(toolbarButton);
          }
          return toolbarButton;
        }),
      ),
      this.ce('div', {
          class: 'btn-group formio-sketchpad-toolbar-group',
          role: 'group'
        },
        this.styles.map(button => {
          const toolbarButtonIcon = this.ce('i', {
            class: `fa fa-${button.icon}`,
          });
          const toolbarButton = this.ce('div', {
            class: `btn btn-secondary formio-sketchpad-toolbar-button formio-sketchpad-toolbar-button-${button.property}`,
            title: button.title
          }, toolbarButtonIcon);
          if (button.attach) {
            return button.attach(toolbarButton);
          }
          return toolbarButton;
        }),
      ),
      this.ce('div', {
          class: 'btn-group float-right formio-sketchpad-toolbar-group',
          role: 'group'
        },
        this.actions.map(button => this.ce('div', {
          class: `btn btn-secondary formio-sketchpad-toolbar-button formio-sketchpad-toolbar-button-${button.action}`,
          onClick: () => this[button.action](),
          title: button.title
        }, this.ce('i', {
          class: `fa fa-${button.icon}`,
        }))),
      ),
    ]);
    /* eslint-enable max-len */
  }

  attach(element) {
    // Disable for now.
    return super.attach(element);
  }

  attachOld(element) {
    this.loadRefs(element, {
      sketchpadContainer: 'single',
      sketchpadCanvas: 'single',
      sketchpadBackground: 'single',
    });

    this.addEventListener(this.refs.sketchpadContainer, 'click', () => this.editSvg());

    //init two instance

    // this.two = new Two({
    //   type: Two.Types.svg,
    // }).appendTo(this.editSketchpad.canvas.container);
    // //init canvas SVG variable
    // this.editSketchpad.canvas.svg = this.two.renderer.domElement;
    // this.addClass(this.editSketchpad.canvas.svg, 'formio-sketchpad-svg');

    this.addBackground();
    this.backgroundReady.promise.then(() => {
      this.backgroundReady.isReady = true;
      this.attach();
      // Disable if needed.
      if (this.shouldDisable) {
        this.disabled = true;
      }

      // Restore the value.
      this.restoreValue();

      this.autofocus();

      this.attachLogic();
    });

    // Set up mouse events.
    this.editSketchpad.canvas.svg
      .addEventListener('mousedown', (e) => {
        e.preventDefault();
        const offset = this.editSketchpad.canvas.svg.getBoundingClientRect();
        //change cursor
        let cursor = 'default';
        if (this.modes[this.state.mode].cursor) {
          cursor = this.modes[this.state.mode].cursor.clicked || this.modes[this.state.mode].cursor.hover;
        }
        this.editSketchpad.canvas.svg.style.cursor = cursor;
        if (this.modes[this.state.mode].eventStart) {
          this.modes[this.state.mode].eventStart(this.getActualCoordinate({
            x: e.clientX - offset.left,
            y: e.clientY - offset.top
          }));
        }

        const mouseDrag = (e) => {
          e.preventDefault();
          const offset = this.editSketchpad.canvas.svg.getBoundingClientRect();
          if (this.modes[this.state.mode].drag) {
            this.modes[this.state.mode].drag(this.getActualCoordinate({
              x: e.clientX - offset.left,
              y: e.clientY - offset.top
            }));
          }
        };

        const mouseEnd = (e) => {
          e.preventDefault();

          this.editSketchpad.canvas.svg
            .removeEventListener('mousemove', mouseDrag);
          this.editSketchpad.canvas.svg
            .removeEventListener('mouseup', mouseEnd);
          //change cursor
          let cursor = 'default';
          if (this.modes[this.state.mode].cursor) {
            cursor = this.modes[this.state.mode].cursor.hover || cursor;
          }
          this.editSketchpad.canvas.svg.style.cursor = cursor;
          const offset = this.editSketchpad.canvas.svg.getBoundingClientRect();
          if (this.modes[this.state.mode].eventEnd) {
            this.modes[this.state.mode].eventEnd(this.getActualCoordinate({
              x: e.clientX - offset.left,
              y: e.clientY - offset.top
            }));
          }
        };

        this.editSketchpad.canvas.svg
          .addEventListener('mousemove', mouseDrag);

        this.editSketchpad.canvas.svg
          .addEventListener('mouseup', mouseEnd);

        return false;
      });

    // Set up touch events.
    this.editSketchpad.canvas.svg
      .addEventListener('touchstart', (e) => {
        e.preventDefault();

        const offset = this.editSketchpad.canvas.svg.getBoundingClientRect();
        const touch = e.changedTouches[0];
        //change cursor
        let cursor = 'default';
        if (this.modes[this.state.mode].cursor) {
          cursor = this.modes[this.state.mode].cursor.clicked || this.modes[this.state.mode].cursor.hover;
        }
        this.editSketchpad.canvas.svg.style.cursor = cursor;
        if (this.modes[this.state.mode].eventStart) {
          this.modes[this.state.mode].eventStart(this.getActualCoordinate({
            x: touch.pageX - offset.left,
            y: touch.pageY - offset.top
          }));
        }

        const touchDrag = (e) => {
          e.preventDefault();

          const offset = this.editSketchpad.canvas.svg.getBoundingClientRect();
          const touch = e.changedTouches[0];
          if (this.modes[this.state.mode].drag) {
            this.modes[this.state.mode].drag(this.getActualCoordinate({
              x: touch.pageX - offset.left,
              y: touch.pageY - offset.top
            }));
          }
        };

        const touchEnd = (e) => {
          e.preventDefault();

          this.editSketchpad.canvas.svg
            .removeEventListener('touchmove', touchDrag);
          this.editSketchpad.canvas.svg
            .removeEventListener('touchend', touchEnd);

          const offset = this.editSketchpad.canvas.svg.getBoundingClientRect();
          const touch = e.changedTouches[0];
          //change cursor
          let cursor = 'default';
          if (this.modes[this.state.mode].cursor) {
            cursor = this.modes[this.state.mode].cursor.hover || cursor;
          }
          this.editSketchpad.canvas.svg.style.cursor = cursor;
          if (this.modes[this.state.mode].eventEnd) {
            this.modes[this.state.mode].eventEnd(this.getActualCoordinate({
              x: touch.pageX - offset.left,
              y: touch.pageY - offset.top
            }));
          }
        };

        this.editSketchpad.canvas.svg
          .addEventListener('touchmove', touchDrag);

        this.editSketchpad.canvas.svg
          .addEventListener('touchend', touchEnd);

        return false;
      });

    this.two.update();
  }

  get dataReady() {
    return this.backgroundReady.promise;
  }

  addBackground() {
    if (this.component.image) {
      this.setBackgroundImage(this.component.image);
      this.backgroundReady.resolve();
    }
    else if (this.component.imageUrl) {
      Formio.makeStaticRequest(this.component.imageUrl, 'GET', null, { noToken: true, headers: {} })
        .then(image => {
          this.setBackgroundImage(image);
          this.backgroundReady.resolve();
        })
        .catch(() => {
          console.warn(`Sketchpad background didn't load for component: ${this.component.key}`);
          this.backgroundReady.resolve();
        });
    }
    //TODO make sure component works without background
  }

  /* eslint-disable max-statements */
  setBackgroundImage(svgMarkup) {
    const xmlDoc = new DOMParser().parseFromString(svgMarkup, 'image/svg+xml');
    let backgroundSvg = xmlDoc.getElementsByTagName('svg');
    if (!backgroundSvg || !backgroundSvg[0]) {
      console.warn(`Sketchpad '${this.component.key}': Background SVG doesn't contain <svg> tag on it`);
      return;
    }
    backgroundSvg = backgroundSvg[0];
    if (this.useBackgroundDimensions) {
      const viewBox = backgroundSvg.getAttribute('viewBox');
      let viewBoxMinX, viewBoxMinY, viewBoxWidth, viewBoxHeight;
      if (viewBox) {
        [viewBoxMinX, viewBoxMinY, viewBoxWidth, viewBoxHeight] = viewBox.split(' ').map(parseFloat);
      }
      else {
        //if viewBox is not defined, use 'x', 'y', 'width' and 'height' SVG attributes (or 0, 0, 640, 480 relatively if any is not defined)
        [viewBoxMinX, viewBoxMinY, viewBoxWidth, viewBoxHeight] = [
          { attribute: 'x', defaultValue: 0 },
          { attribute: 'y', defaultValue: 0 },
          { attribute: 'width', defaultValue: 640 },
          { attribute: 'height', defaultValue: 480 }
        ].map(dimension => {
          return parseFloat(backgroundSvg.getAttribute(dimension.attribute)) || dimension.defaultValue;
        });
      }
      //set  dimensions to width and height from viewBox of background svg
      this.dimensions = {
        width: viewBoxWidth,
        height: viewBoxHeight
      };
      //set default and current viewBox sizes for canvas and background (should be based on background)
      this.zoomInfo.viewBox.default = {
        width: this.dimensions.width,
        height: this.dimensions.height,
        minX: viewBoxMinX,
        minY: viewBoxMinY
      };
    }
    else {
      //set dimensions to component width and height
      this.dimensions = {
        width: this.component.width,
        height: this.component.height
      };
      let viewBoxValue = backgroundSvg.getAttribute('viewBox');

      if (!viewBoxValue) {
        // since zooming works based on viewBox, we need to have explicitly defined value for it
        // if viewBox is not defined on SVG element, browser behaves like it's equal to "0 0 <current_width> <current_height>"
        // since background image should match dimensions of editor image, current width and height will always be equal to component.width and component.height
        // as a result:
        viewBoxValue = `0 0 ${this.dimensions.width} ${this.dimensions.height}`;
        backgroundSvg.setAttribute('viewBox', viewBoxValue);
      }
      let [initialMinX, initialMinY, initialWidth, initialHeight] = viewBoxValue.split(' ').map(parseFloat);
      initialMinX = initialMinX || 0;
      initialMinY = initialMinY || 0;
      initialWidth = initialWidth || this.dimensions.width;
      initialHeight = initialHeight || this.dimensions.height;
      const width = this.dimensions.width,
        height = this.dimensions.height,
        minX = Math.round(initialMinX - (this.dimensions.width - initialWidth) / 2),
        minY = Math.round(initialMinY - (this.dimensions.height - initialHeight) / 2);
      //set initial zoom info for SVG
      this.zoomInfo.viewBox.default = {
        width: width,
        height: height,
        minX: minX,
        minY: minY
      };
    }
    //set current zoom to default
    this.zoomInfo.viewBox.current = _.cloneDeep(this.zoomInfo.viewBox.default);

    svgMarkup = new XMLSerializer().serializeToString(backgroundSvg);
    //fix weird issue in Chrome when it returned '<svg:svg>...</svg:svg>' string after serialization instead of <svg>...</svg>
    svgMarkup = svgMarkup.replace('<svg:svg', '<svg').replace('</svg:svg>', '</svg>');

    // this.editSketchpad.background.container.style.minWidth = `${this.dimensions.width}px`;
    // this.editSketchpad.background.container.style.minHeight = `${this.dimensions.height}px`;

    //set background containers content to SVG markup
    this.refs.sketchpadContainer.innerHTML = svgMarkup;
    // this.editSketchpad.background.container.innerHTML = svgMarkup;
    //init svg variables
    this.sketchpadBackground.svg = this.viewSketchpad.background.container.firstElementChild;
    // this.editSketchpad.background.svg = this.editSketchpad.background.container.firstElementChild;

    //set background image viewBox
    const viewBox = this.zoomInfo.viewBox.current;
    this.sketchpadBackground.svg.setAttribute('viewBox', `${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}`);
    // this.editSketchpad.background.svg.setAttribute('viewBox', `${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}`);
    //set canvas image viewBox (necessary at least for useBackgroundDimensions when background image has minX and minY other that 0
    // this.editSketchpad.canvas.svg.setAttribute('viewBox', `${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}`);

    //set dimensions for Two.js instance
    this.setEditorSize(this.dimensions.width, this.dimensions.height);
  }

  /* eslint-enable max-statements */

  clear() {
    this.two.clear();
  }

  clearAll() {
    this.layers = [];
    this.editValue = [];
    this.clear();
    this.two.update();
  }

  draw(value) {
    this.clear();
    const layers = value.map(item => this.modes[item.mode].draw(item));
    this.two.update();
    this.layers = layers;
    if (layers.length) {
      layers.forEach((layer, index) => {
        layer._renderer.elem.addEventListener('click', (e) => this.click(e, index));
      });
    }
  }

  click(event, index) {
    console.log(event, index);
  }

  undo() {
    const value = this.editValue.slice();
    if (value.length === 0) {
      return;
    }
    this.deleted.push(value.pop());
    this.editValue = value;
    this.triggerChange();
    this.draw(value);
  }

  redo() {
    if (this.deleted.length === 0) {
      return;
    }
    const value = this.editValue.slice();
    value.push(this.deleted.pop());
    this.editValue = value;
    this.triggerChange();
    this.draw(value);
  }

  setState(state) {
    Object.assign(this.state, state);
    this.setActiveButton(this.state.mode);
    //change cursor
    this.editSketchpad.canvas.svg.style.cursor = _.get(this.modes[this.state.mode], 'cursor.hover', 'default');
  }

  setActiveButton(mode) {
    this.modeButtons.forEach(button => this.removeClass(button, 'active'));
    Object.keys(this.modes).forEach((key, index) => {
      if (this.modes[key].state.mode === mode) {
        this.addClass(this.modeButtons[index], 'active');
      }
    });
  }

  setValue(value) {
    if (!this.backgroundReady.isReady || !this.two) {
      return;
    }
    this.draw(value);
    this.copySvgToView();
  }

  copySvgToView() {
    //clone view SVG element from editor
    const svgElement = this.editSketchpad.canvas.svg.cloneNode(true);
    //make view SVG responsive: remove height and width attribute, add viewBox attribute
    svgElement.removeAttribute('height');
    svgElement.removeAttribute('width');
    svgElement.style.cursor = 'pointer';
    //set viewBox to default to reset zoom
    const viewBox = this.zoomInfo.viewBox.default;
    svgElement.setAttribute('viewBox', `${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}`);
    this.viewSketchpad.canvas.container.innerHTML = '';
    this.viewSketchpad.canvas.container.appendChild(svgElement);
  }

  zoom(coordinate, multiplier) {
    this.setTotalMultiplier(this.zoomInfo.totalMultiplier * multiplier);
    //calculate new viewBox width for canvas
    this.zoomInfo.viewBox.current.width =
      Math.round(this.zoomInfo.viewBox.default.width / this.zoomInfo.totalMultiplier);
    this.zoomInfo.viewBox.current.height =
      Math.round(this.zoomInfo.viewBox.default.height / this.zoomInfo.totalMultiplier);
    if (
      this.zoomInfo.viewBox.current.width > this.zoomInfo.viewBox.default.width &&
      this.zoomInfo.viewBox.current.height > this.zoomInfo.viewBox.default.height
    ) {
      //if should get less than initial size, change editor size instead of viewBox size
      this.setEditorSize(
        this.dimensions.width * this.zoomInfo.totalMultiplier,
        this.dimensions.height * this.zoomInfo.totalMultiplier
      );
      //restore default viewBox values for canvas and background
      this.zoomInfo.viewBox.current = _.cloneDeep(this.zoomInfo.viewBox.default);
    }
    else {
      //if should get more than initial size, change viewBox size
      //restore editor size if needed
      if (this.two.width !== this.dimensions.width || this.two.height !== this.dimensions.height) {
        this.setEditorSize(this.dimensions.width, this.dimensions.height);
      }
      //calculate SVG offset so that coordinate would be center of zoomed image
      this.zoomInfo.viewBox.current.minX = coordinate.x - this.zoomInfo.viewBox.current.width / 2;
      this.zoomInfo.viewBox.current.minY = coordinate.y - this.zoomInfo.viewBox.current.height / 2;
      this.normalizeSvgOffset();
    }
    this.updateSvgViewBox();
  }

  resetZoom() {
    this.zoom({ x: 0, y: 0 }, (this.component.defaultZoom / 100) / this.zoomInfo.totalMultiplier);
  }

  getActualCoordinate(coordinate) {
    //recalculate coordinate taking into account current zoom
    coordinate.x = Math.round((coordinate.x / this.zoomInfo.totalMultiplier / this.dimensionsMultiplier) + this.zoomInfo.viewBox.current.minX);
    coordinate.y = Math.round((coordinate.y / this.zoomInfo.totalMultiplier / this.dimensionsMultiplier) + this.zoomInfo.viewBox.current.minY);
    return coordinate;
  }

  dragImage(offset) {
    //calculate new offsets for SVG
    this.zoomInfo.viewBox.current.minX = this.zoomInfo.viewBox.current.minX - offset.x;
    this.zoomInfo.viewBox.current.minY = this.zoomInfo.viewBox.current.minY - offset.y;
    this.normalizeSvgOffset();
    this.updateSvgViewBox();
  }

  normalizeSvgOffset() {
    /* eslint-disable max-len */
    //don't let offset go out of SVG on the left and on the top
    //canvas
    this.zoomInfo.viewBox.current.minX = this.zoomInfo.viewBox.current.minX < this.zoomInfo.viewBox.default.minX ? this.zoomInfo.viewBox.default.minX : this.zoomInfo.viewBox.current.minX;
    this.zoomInfo.viewBox.current.minY = this.zoomInfo.viewBox.current.minY < this.zoomInfo.viewBox.default.minY ? this.zoomInfo.viewBox.default.minY : this.zoomInfo.viewBox.current.minY;
    //don't let offset go out of SVG on the right and on the bottom
    //canvas
    const canvasMaxOffsetX = this.zoomInfo.viewBox.default.width - this.zoomInfo.viewBox.current.width + this.zoomInfo.viewBox.default.minX,
      canvasMaxOffsetY = this.zoomInfo.viewBox.default.height - this.zoomInfo.viewBox.current.height + this.zoomInfo.viewBox.default.minY;
    this.zoomInfo.viewBox.current.minX = this.zoomInfo.viewBox.current.minX > (canvasMaxOffsetX) ? canvasMaxOffsetX : this.zoomInfo.viewBox.current.minX;
    this.zoomInfo.viewBox.current.minY = this.zoomInfo.viewBox.current.minY > (canvasMaxOffsetY) ? canvasMaxOffsetY : this.zoomInfo.viewBox.current.minY;
    /* eslint-enable max-len */
  }

  updateSvgViewBox() {
    //set viewBox so that SVG gets zoomed to the proper area according to zoomInfo
    const viewBox = this.zoomInfo.viewBox.current;
    this.editSketchpad.canvas.svg.setAttribute('viewBox', `${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}`);
    this.editSketchpad.background.svg.setAttribute('viewBox', `${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}`);
  }

  setTotalMultiplier(multiplier) {
    this.zoomInfo.totalMultiplier = multiplier;
    this.totalMultiplierElement.innerHTML = this.t(Math.round(multiplier * 100) / 100);
  }

  setEditorSize(width, height) {
    this.two.width = width;
    this.two.height = height;
    this.two.update();
    //change width of background svg so it matches editor SVG
    this.editSketchpad.background.svg.style.width = width;
    this.editSketchpad.background.svg.style.height = height;
    this.editSketchpad.background.container.style.minWidth = `${width}px`;
    this.editSketchpad.background.container.style.minHeight = `${height}px`;
  }
}