tracking-code-which-will-go-to-the-HEAD draw/DefaultDrawer.js

Source

draw/DefaultDrawer.js

import * as d3 from 'd3';
import DeleteAction from './action/DeleteAction';
import DragComponentAction from './action/DragComponentAction';
import DragSceneAction from './action/DragSceneAction';
import LinkAction from './action/LinkAction';
import MenuAction from './action/MenuAction';
import ResizeComponentAction from './action/ResizeComponentAction';
import ToggleSelectionAction from './action/ToggleSelectionAction';
import DeselectionAllAction from './action/DeselectionAllAction';
import SelectionAllAction from './action/SelectionAllAction';
import ZoomAction from './action/ZoomAction';
import ComponentRenderer from './render/ComponentRenderer';
import LinkRenderer from './render/LinkRenderer';
import CustomLayout from './layout/CustomLayout';

/**
 * Class that draws a component in a graphical representation.
 */
class DefaultDrawer {
  /**
   * Default constructor
   * @param {DefaultData} pluginData - Plugin data storage.
   */
  constructor(pluginData) {
    /**
     * Plugin data storage.
     * @type {DefaultData}
     */
    this.pluginData = pluginData;

    /**
     * Component renderer.
     */
    this.componentRenderer = null;

    /**
     * Link renderer.
     */
    this.linkRenderer = null;

    /**
     * Plugin layout system.
     * @type {DefaultLayout}
     */
    this.layout = null;

    /**
     * D3 selection of the view port.
     * @type {Selection}
     */
    this.viewport = null;

    /**
     * D3 selection of the main scene.
     * @type {Selection}
     */
    this.scene = null;

    /**
     * D3 library.
     * @type {object}
     */
    this.d3 = d3;

    /**
     * Object to store all instances of actions.
     * @type {object}
     */
    this.actions = {
      deleteAction: null,
      dragComponent: null,
      dragScene: null,
      linkAction: null,
      menuAction: null,
      resizeComponent: null,
      toggleSelection: null,
      selectionAll: null,
      deselectionAll: null,
      zoom: null,
    };

    /**
     * Object to store all states of the scene.
     * @type {object}
     */
    this.states = {
      linking: false,
      menuOpening: false,
      moving: false,
      resizing: false,
      selection: false,
    };
    this.readOnly = false;
  }

  /**
   * Initialize drawing context.
   * @param {string} id - Html id of div that will be the drawing container.
   * @param {boolean} readOnly - Indicate if user can make action or modify the scene.
   */
  init(id, readOnly = false) {
    this.d3.select(`#${id}`).selectAll(null).remove();
    this.d3.select(`#${id}`).html('');
    this.viewport = this.d3.select(`#${id}`);

    this.componentRenderer = this.initComponentRenderer(readOnly);
    this.linkRenderer = this.initLinkRenderer(readOnly);
    this.layout = new CustomLayout(
      this.pluginData,
      this.viewport,
    );
    this.readOnly = readOnly;
    this.clearActions();

    if (this.readOnly) {
      this.pluginData.scene.x = 0;
      this.pluginData.scene.y = 0;
      this.pluginData.scene.zoom = 1;
    }

    this.initActions();
    this.initScene();
  }

  /**
   * Initialize component renderer and return instance of it.
   * @param {boolean} readOnly - Indicate if user can make action or modify the scene.
   * @returns {ComponentRenderer} New instance of ComponentRenderer.
   */
  initComponentRenderer(readOnly) {
    return new ComponentRenderer(this.pluginData, this.viewport, readOnly);
  }

  /**
   * Initialize link renderer and return instance of it.
   * @param {boolean} readOnly - Indicate if user can make action or modify the scene.
   * @returns {LinkRenderer} New instance of LinkRenderer.
   */
  initLinkRenderer(readOnly) {
    return new LinkRenderer(this.pluginData, this.viewport, readOnly);
  }

  /**
   * Remove all instances of actions and remove all set events.
   */
  clearActions() {
    this.actions.deleteAction = null;
    this.actions.dragComponent = null;
    this.actions.dragScene = null;
    this.actions.menuAction = null;
    this.actions.resizeComponent = null;
    this.actions.toggleSelection = null;
    this.actions.selectionAll = null;
    this.actions.deselectionAll = null;
    this.actions.zoom = null;

    this.viewport.on('wheel', null);
    this.d3.select('body')
      .on('keydown', null)
      .on('keyup', null);
    this.viewport.on('.drag', null);
  }

  /**
   * Initialize actions and set event on viewport and other.
   */
  initActions() {
    this.actions.selectionAll = new SelectionAllAction(this.pluginData, this.viewport, this.layout);
    this.actions.deleteAction = new DeleteAction(this.pluginData, this.viewport, this.layout);
    this.actions.dragScene = new DragSceneAction(this.pluginData, this.viewport, this.layout);
    this.actions.menuAction = new MenuAction(this.pluginData, this.viewport, this.layout);
    this.actions.link = new LinkAction(this.pluginData, this.viewport, this.layout);
    this.actions.zoom = new ZoomAction(this.pluginData, this.viewport, this.layout);
    this.actions.dragComponent = new DragComponentAction(
      this.pluginData,
      this.viewport,
      this.layout,
    );
    this.actions.resizeComponent = new ResizeComponentAction(
      this.pluginData,
      this.viewport,
      this.layout,
    );
    this.actions.toggleSelection = new ToggleSelectionAction(
      this.pluginData,
      this.viewport,
      this.layout,
    );
    this.actions.deselectionAll = new DeselectionAllAction(
      this.pluginData,
      this.viewport,
      this.layout,
    );

    this.viewport.on('wheel', (event) => {
      const haveToDraw = this.actions.zoom.execute(event);

      if (haveToDraw) {
        this.draw();
      }
    });

    if (!this.readOnly) {
      this.d3.select('body')
        .on('keydown', (event) => {
          let haveToDraw = false;

          if (this.pluginData.configuration.keysBinding.selection.includes(event.key)) {
            this.states.selection = true;
          }
          if (this.pluginData.configuration.keysBinding.deleteObject.includes(event.key)) {
            haveToDraw = this.actions.deleteAction.execute(event);
          }
          if (this.pluginData.configuration.keysBinding.selectAll.includes(event.key)) {
            haveToDraw = this.actions.selectionAll.execute(event);
          }
          if (this.pluginData.configuration.keysBinding.deselectAll.includes(event.key)) {
            haveToDraw = this.actions.deselectionAll.execute(event);
          }

          if (haveToDraw) {
            this.draw();
          }
        })
        .on('keyup', (event) => {
          let haveToDraw = false;

          if (this.pluginData.configuration.keysBinding.selection.includes(event.key)) {
            this.states.selection = false;
          }
          if (this.pluginData.configuration.keysBinding.deleteObject.includes(event.key)) {
            haveToDraw = this.actions.deleteAction.finalize(event);
          }
          if (this.pluginData.configuration.keysBinding.selectAll.includes(event.key)) {
            haveToDraw = this.actions.selectionAll.finalize(event);
          }
          if (this.pluginData.configuration.keysBinding.deselectAll.includes(event.key)) {
            haveToDraw = this.actions.deselectionAll.finalize(event);
          }

          if (haveToDraw) {
            this.draw();
          }
        });
    }

    const drag = this.d3.drag()
      .subject((event) => {
        const element = event.sourceEvent.target.closest('.component');

        this.states.resizing = !!event.sourceEvent.target.closest('.resize-button');
        this.states.menuOpening = !!event.sourceEvent.target.closest('.menu-button');

        if (!element) {
          return this.scene;
        }
        const subject = this.viewport.select(Array.from(element.classList)
          .map((value) => `.${value}`)
          .join(''));

        this.states.linking = !!event.sourceEvent.target.closest('.anchor')
          && this.pluginData.canHaveLink(subject.datum().data.definition.type);

        return subject;
      })
      .on('drag', (event) => {
        this.states.moving = true;

        const isScene = event.subject.datum().data.id === 'scene';
        let haveToDraw = false;

        if (this.readOnly) {
          this.actions.dragScene.execute(event);

          return;
        }

        if (isScene) {
          haveToDraw = this.actions.dragScene.execute(event);
        } else if (this.states.linking) {
          haveToDraw = this.actions.link.execute(event);
          this.linkRenderer.render();
        } else if (this.states.resizing) {
          haveToDraw = this.actions.resizeComponent.execute(event);
          this.linkRenderer.render();
        } else {
          haveToDraw = this.actions.dragComponent.execute(event);
          this.linkRenderer.render();
        }

        if (haveToDraw) {
          this.draw();
        }
      })
      .on('end', async (event) => {
        const hasMoved = this.states.moving;
        const isSelected = this.states.selection;
        const isMenuOpening = this.states.menuOpening;
        const isResizing = this.states.resizing;
        const isLinking = this.states.linking;
        const isScene = event.subject.datum().data.id === 'scene';
        let haveToDraw = false;

        if (this.readOnly) {
          if (isScene && hasMoved) {
            this.actions.dragScene.finalize(event);
          }

          return;
        }

        if (isLinking) {
          // LINKING PARK - in the end!!!!
          haveToDraw = this.actions.link.finalize(event);
        } else if (hasMoved && isScene) {
          haveToDraw = this.actions.dragScene.finalize(event);
        } else if (isResizing) {
          haveToDraw = this.actions.resizeComponent.finalize(event);
        } else if (hasMoved && !isScene) {
          haveToDraw = this.actions.dragComponent.finalize(event);
        } else if (!hasMoved && isMenuOpening) {
          haveToDraw = this.actions.menuAction.finalize(event);
        } else if (!hasMoved && !isScene && isSelected) {
          haveToDraw = this.actions.toggleSelection.finalize(event);
        }

        if (haveToDraw) {
          this.draw();
        }

        this.states.moving = false;
      });

    this.viewport.call(drag);
  }

  /**
   * Get scene width data.
   * @returns {object} D3 selection of scene.
   */
  getScene() {
    return this.viewport
      .selectAll('.scene')
      .data(this.getSceneData())
      .join('svg')
      .attr('class', 'scene')
      .attr('width', '100%')
      .attr('height', '100%');
  }

  /**
   * Initialize the scene, set all markers component and link groups.
   */
  initScene() {
    this.viewport
      .append('style')
      .text(this.pluginData.resources.style);
    this.scene = this.getScene();
    this.scene.attr('version', '1.1');
    this.scene.attr('xmlns', 'http://www.w3.org/2000/svg');

    this.scene.append('defs').html(
      Object.keys(this.pluginData.resources.markers)
        .map((key) => this.pluginData.resources.markers[key])
        .join(''),
    );
    const { x, y, zoom } = this.pluginData.scene;
    const transform = `translate(${x} ${y}) scale(${zoom})`;

    this.scene.append('g')
      .attr('class', 'components')
      .attr('transform', transform);
    this.scene.append('g')
      .attr('class', 'links')
      .attr('transform', transform);
  }

  /**
   * Components driven drawing.
   */
  draw() {
    this.scene = this.getScene();

    this.componentRenderer.render('scene');
    this.linkRenderer.render('scene');

    if (this.readOnly) {
      const { width: vWidth, height: vHeight } = this.viewport.node().getBoundingClientRect();
      const {
        width,
        height,
        x,
        y,
      } = this.viewport.select('.scene .components').node().getBBox();
      const ratio = vWidth / width < vHeight / height ? vWidth / width : vHeight / height;
      let zoom = 1;

      while (zoom > ratio) {
        zoom *= 0.9;
      }

      this.scene.attr('viewBox', `${x - 15} ${y - 15} ${width + x + 15} ${height + y + 15}`);
      this.pluginData.scene.zoom = zoom;
    }
  }

  /**
   * Reorganize position and size of all components.
   * This method does not refresh the view. You have to await it and trigger a redraw.
   * @param {string} id - The container within which we need to organize the children,
   * and if not specified, all components will be reorganized.
   * @param {boolean} keepPosition -  If true only component without position will be reorganized.
   */
  arrangeComponentsPosition(id, keepPosition) {
    this.layout.generateComponentsLayout(id, keepPosition);
  }

  /**
   * Resize the container to its minimum size.
   * @param {string} id - Container id.
   */
  resize(id) {
    this.layout.resize(id);
  }

  /**
   * Format component dataset to d3 hierarchy.
   * @returns {object} - Formated component dataset.
   */
  getSceneData() {
    return [
      this.d3.hierarchy(
        {
          id: 'scene',
          name: '',
          children: [
            ...this.pluginData.components
              .filter((component) => component.getContainerId() === null),
          ],
          ...this.pluginData.scene,
        },
        (data) => {
          if (data.id === 'scene') {
            return data.children;
          }

          return this.pluginData.getChildren(data.id);
        },
      ),
    ];
  }

  /**
   * Drop component on scene.
   * @param {Component} component - Component to drop.
   * @param {object} event - Mouse event.
   */
  dropComponent(component, event) {
    const target = event.target.closest('.component');

    component.drawOption.width = component.definition.width;
    component.drawOption.height = component.definition.height;

    if (!target) {
      const { x: sceneX, y: sceneY } = this.viewport.select('.scene')
        .node().getBoundingClientRect();

      component.drawOption.x = (event.clientX - sceneX - this.pluginData.scene.x)
        / this.pluginData.scene.zoom - component.definition.defaultWidth / 2;
      component.drawOption.y = (event.clientY - sceneY - this.pluginData.scene.y)
        / this.pluginData.scene.zoom - component.definition.defaultHeight / 2;

      return;
    }

    if (target.canContain(component.definition.type)) {
      const { x: containerX, y: containerY } = this.viewport
        .select(`.${component.id}.container`).node().getBoundingClientRect();

      component.drawOption.x = (event.clientX - containerX - this.pluginData.scene.x)
        / this.pluginData.scene.zoom - component.definition.defaultWidth / 2;
      component.drawOption.y = (event.clientY - containerY - this.pluginData.scene.y)
        / this.pluginData.scene.zoom - component.definition.defaultHeight / 2;
    }
  }

  /**
   * Export viewport as svg.
   * @param {string} id - Viewport id.
   * @returns {string} Svg content.
   */
  exportSvg(id) {
    this.viewport.selectAll('.scene > g').attr('transform', '');

    return document.querySelector(`#${id} .scene`).outerHTML;
  }
}

export default DefaultDrawer;