renderers/shared/Card.js

// Libraries
const React = require("react");
const PropTypes = require("prop-types");

// RVG Elements
const {
  SVG,
  Text,
  Rectangle,
  Circle,
  Ellipse,
  Line,
  Image,
  Path,
  LinearGradient,
} = require("../../rvg/elements");

/**
 * @name Card
 * @class The Card React element
 */
class Card extends React.Component {
  constructor(props) {
    super(props);
  }

  /**
   * Calculates the Y position of an element based on any attachments etc.
   *
   * @param {object} layers - The object of all layers
   * @param {object} layer - The layer to calculate the Y position for
   *
   * @return {integer} The Y position
   */
  calculateYPosition(layers, layer) {
    // Get the layer's currently configured Y position
    let attachYLayerPosition = this.getLayerValue(layers, layer, "y");

    // If this is an object and has the attach property
    if (
      typeof attachYLayerPosition === "object" &&
      attachYLayerPosition.attach !== "undefined"
    ) {
      // Get the layer to attach to
      let attachYLayer = layers[layer.y.attach];

      // Calculate the Y offset
      let attachYLayerHeight = 0;
      switch (attachYLayer.type) {
        case "text":
          // eslint-disable-next-line
          let attachYLayerText = attachYLayer.text.split("\n");
          if (attachYLayer.text !== "") {
            attachYLayerHeight =
              attachYLayerText.length *
              (this.getLayerValue(layers, attachYLayer, "lineHeight") ||
                this.getLayerValue(layers, attachYLayer, "fontSize"));
          }
          break;
        default:
          if (
            typeof this.getLayerValue(layers, attachYLayer, "height") !==
            "undefined"
          ) {
            attachYLayerHeight = this.getLayerValue(
              layers,
              attachYLayer,
              "height"
            );
          }
          break;
      }

      // Add any additionally configured offset value
      let attachYLayerOffset = layer.y.offset || 0;

      // Add them together and recursively call this function if the next layer has an attachment
      attachYLayerPosition =
        attachYLayerHeight +
        this.calculateYPosition(layers, attachYLayer) +
        attachYLayerOffset;
    }

    // Return the value
    return attachYLayerPosition;
  }

  /**
   * Returns the value for a given layer property
   *
   * @param {object} layers - The object of all layers
   * @param {object} layer - The layer to get the value for
   * @param {object} key - The key of the value to get from the layer
   *
   * @return {mixed} The value of the property on the layer
   */
  getLayerValue(layers, layer, key) {
    if (typeof layer[key] === "function") {
      return layer[key](layers, this.props.svgRef);
    }

    return layer[key];
  }

  /**
   * Compute the gradient elements to render to the <defs> element
   *
   * @param {object} layers - The configuration object representing the layers that may require gradients
   *
   * @return {array} An array of React elements to render to the <defs> element
   */
  computeGradients(layers) {
    const array = [];
    let layer, gradient;

    Object.keys(layers).forEach((key) => {
      layer = layers[key];

      if (this.getLayerValue(layers, layer, "gradient")) {
        gradient = this.getLayerValue(layers, layer, "gradient");

        array.push(
          <LinearGradient
            key={key}
            name={key}
            x1={0}
            x2={0}
            y1={0}
            y2={1}
            stops={gradient}
          />
        );
      }
    });

    return array;
  }

  /**
   * Compute the layers to render on the Card
   *
   * @param {object} layers - The configuration object representing the layers to render
   *
   * @return {array} An array of React elements to render on the card
   */
  computeLayers(layers) {
    const array = [];
    let layer;

    // Iterate over the layers
    Object.keys(layers).forEach((key) => {
      layer = layers[key];

      // If the layer is hidden, ignore it
      if (this.getLayerValue(layers, layer, "hidden") === true) {
        return;
      }

      // Setup an object to contain our layer data
      const layerData = {};

      // Iterate over the properties of the layer, and compute the value (handles getters, functions, and object implementations such as `y`)
      Object.keys(layer).forEach((k) => {
        layerData[k] = this.getLayerValue(layers, layer, k);
      });

      // Make the fill value map to a gradient name, if a gradient has been configured
      // See computeGradients() for the creation of gradient definitions
      if (this.getLayerValue(layers, layer, "gradient")) {
        layerData.fill = `url(#${key})`;
      }

      // Switch over the layer type to ensure we create the card correctly
      switch (layer.type) {
        case "text":
          // Split by newline
          // eslint-disable-next-line
          const text = layerData.text.split("\n");

          array.push(
            <Text
              x={layerData.x}
              y={this.calculateYPosition(layers, layerData)}
              fontFamily={layerData.fontFamily}
              fontSize={layerData.fontSize}
              fontWeight={layerData.fontWeight}
              lineHeight={layerData.lineHeight}
              textAnchor={layerData.textAnchor}
              fill={layerData.fill}
              draggable={layerData.draggable}
              transform={layerData.transform}
              opacity={layerData.opacity}
              smartQuotes={layerData.smartQuotes}
              key={key}
            >
              {text}
            </Text>
          );
          break;
        case "image":
          array.push(
            <Image
              x={layerData.x}
              y={this.calculateYPosition(layers, layerData)}
              href={layerData.src}
              height={layerData.height}
              width={layerData.width}
              draggable={layerData.draggable}
              transform={layerData.transform}
              opacity={layerData.opacity}
              key={key}
            />
          );
          break;
        case "rectangle":
          array.push(
            <Rectangle
              x={layerData.x}
              y={this.calculateYPosition(layers, layerData)}
              fill={layerData.fill}
              height={layerData.height}
              width={layerData.width}
              draggable={layerData.draggable}
              transform={layerData.transform}
              key={key}
            />
          );
          break;
        case "circle":
          array.push(
            <Circle
              x={layerData.x}
              y={this.calculateYPosition(layers, layerData)}
              fill={layerData.fill}
              radius={layerData.radius}
              draggable={layerData.draggable}
              transform={layerData.transform}
              key={key}
            />
          );
          break;
        case "ellipse":
          array.push(
            <Ellipse
              x={layerData.x}
              y={this.calculateYPosition(layers, layerData)}
              fill={layerData.fill}
              radiusX={layerData.radiusX}
              radiusY={layerData.radiusY}
              draggable={layerData.draggable}
              transform={layerData.transform}
              key={key}
            />
          );
          break;
        case "line":
          array.push(
            <Line
              x={[layerData.x1, layerData.x2]}
              y={[layerData.y1, layerData.y2]}
              stroke={layerData.stroke || layerData.fill}
              draggable={layerData.draggable}
              transform={layerData.transform}
              key={key}
            />
          );
          break;
        case "path":
          array.push(
            <Path
              d={layerData.path || layerData.d}
              fill={layerData.fill}
              draggable={layerData.draggable}
              transform={layerData.transform}
              key={key}
            />
          );
          break;
      }
    });

    return array;
  }

  /**
   * Compute the fonts needed for the card
   *
   * @param {object} fonts - The fonts to use when rendering this card
   *
   * @return {array} An array of React elements to render in the <defs /> element of the SVG
   */
  computeFonts(fonts = {}) {
    return Object.keys(fonts).map((key, index) => {
      let src = fonts[key];
      let format = "svg";
      if (typeof fonts[key] === "object") {
        src = fonts[key].src;
        format = fonts[key].format || "svg";
      }

      return (
        <style key={index}>
          {`@font-face {
              font-family: '${key}';
              src: url("${src}") format("${format}");
              font-weight: normal;
              font-style: normal;
          }`}
        </style>
      );
    });
  }

  /**
   * Renders the card
   *
   * @return {object} JSX for the React Component
   */
  render() {
    // Grab our configuration
    const { card, fonts, layers } = this.props.configuration;

    // Compute layers, gradients and fonts
    const layerArray = this.computeLayers(layers);
    const gradientsArray = this.computeGradients(layers);
    const fontsArray = this.computeFonts(fonts);

    // Return
    return (
      <div
        className="card"
        ref={this.props.svgRef}
        style={{ maxWidth: card.width, maxHeight: card.height }}
      >
        <SVG height={card.height} width={card.width} fill={card.fill}>
          <defs>
            {fontsArray}
            {gradientsArray}
          </defs>

          {layerArray}
        </SVG>
      </div>
    );
  }
}

Card.propTypes = {
  configuration: PropTypes.shape({
    card: PropTypes.object,
    fonts: PropTypes.object,
    layers: PropTypes.object,
  }),
  svgRef: PropTypes.any,
};

// Export
module.exports = Card;