Home Reference Source

src/components/currency/Currency.js

import { createNumberMask } from '@formio/text-mask-addons';
import { maskInput } from '@formio/vanilla-text-mask';
import _ from 'lodash';
import { getCurrencyAffixes } from '../../utils/utils';
import NumberComponent from '../number/Number';

export default class CurrencyComponent extends NumberComponent {
  static schema(...extend) {
    return NumberComponent.schema({
      type: 'currency',
      label: 'Currency',
      key: 'currency'
    }, ...extend);
  }

  static get builderInfo() {
    return {
      title: 'Currency',
      group: 'advanced',
      icon: 'usd',
      documentation: '/userguide/form-building/advanced-components#currency',
      weight: 70,
      schema: CurrencyComponent.schema()
    };
  }

  constructor(component, options, data) {
    // Currency should default to have a delimiter unless otherwise specified.
    if (component && !component.hasOwnProperty('delimiter')) {
      component.delimiter = true;
    }
    super(component, options, data);
  }

  /**
   * Creates the number mask for currency numbers.
   *
   * @return {*}
   */
  createNumberMask() {
    const decimalLimit = _.get(this.component, 'decimalLimit', 2);
    const affixes = getCurrencyAffixes({
      currency: this.component.currency,
      decimalLimit: decimalLimit,
      decimalSeparator: this.decimalSeparator,
      lang: this.options.language
    });
    this.currencyPrefix = this.options.prefix || affixes.prefix;
    this.currencySuffix = this.options.suffix || affixes.suffix;
    return createNumberMask({
      prefix: this.currencyPrefix,
      suffix: this.currencySuffix,
      thousandsSeparatorSymbol: _.get(this.component, 'thousandsSeparator', this.delimiter),
      decimalSymbol: _.get(this.component, 'decimalSymbol', this.decimalSeparator),
      decimalLimit: decimalLimit,
      allowNegative: _.get(this.component, 'allowNegative', true),
      allowDecimal: this.isDecimalAllowed(),
    });
  }

  isDecimalAllowed() {
    return _.get(this.component, 'allowDecimal', true);
  }

  setInputMask(input) {
    const affixes = getCurrencyAffixes({
      currency: this.component.currency,
      decimalSeparator: this.decimalSeparator,
      lang: this.options.language,
    });
    let numberPattern = `${affixes.prefix}[0-9`;
    numberPattern += this.decimalSeparator || '';
    numberPattern += this.delimiter || '';
    numberPattern += ']*';
    input.setAttribute('pattern', numberPattern);
    input.mask = maskInput({
      inputElement: input,
      mask: this.numberMask || '',
      pipe: (conformedValue) => {
        if (conformedValue === '$0._') {
          // Delay to allow mask to update first.
          setTimeout(() => {
            const caretPosition = input.value.length - 1;
            input.setSelectionRange(caretPosition, caretPosition);
          });
        }
        return conformedValue;
      },
      shadowRoot: this.root ? this.root.shadowRoot : null
    });
  }

  get defaultSchema() {
    return CurrencyComponent.schema();
  }

  parseNumber(value) {
    return super.parseNumber(this.stripPrefixSuffix(value));
  }

  parseValue(value) {
    return super.parseValue(this.stripPrefixSuffix(value));
  }

  addZerosAndFormatValue(value) {
   if (!value && value !== 0) return;

    const decimalLimit = _.get(this.component, 'decimalLimit', 2);

    let integerPart;
    let decimalPart = '';
    let decimalPartNumbers = [];
    const negativeValueSymbol = '-';
    const hasPrefix = this.currencyPrefix ? value.includes(this.currencyPrefix) : false;
    const hasSuffix = this.currencySuffix ? value.includes(this.currencySuffix) : false;
    const isNegative = value.includes(negativeValueSymbol) || false;

    value = this.stripPrefixSuffix(isNegative ? value.replace(negativeValueSymbol,'') : value);

    if (value.includes(this.decimalSeparator)) {
      [integerPart, decimalPart] = value.split(this.decimalSeparator);
      decimalPartNumbers =[...decimalPart.split('')] ;
    }
    else {
      integerPart = value;
    }

    if (decimalPart.length < decimalLimit) {
      while (decimalPartNumbers.length < decimalLimit) {
        decimalPartNumbers.push('0');
      }
    }

    const formattedValue = `${isNegative ? negativeValueSymbol:''}${hasPrefix ? this.currencyPrefix : ''}${integerPart}${this.decimalSeparator}${decimalPartNumbers.join('')}${hasSuffix ? this.currencySuffix : ''}`;

    return super.formatValue(formattedValue);
  }

  getValueAsString(value, options) {
    const stringValue = super.getValueAsString(value, options);

    // eslint-disable-next-line eqeqeq
    if (value || value == '0') {
      if (Array.isArray(value)) {
        return value.map((val) => this.addZerosAndFormatValue(super.getValueAsString(val, options))).join(', ');
      }
      return this.addZerosAndFormatValue(stringValue);
    }

    return stringValue;
  }

  formatValue(value) {
    if (value || value === '0') {
      return this.addZerosAndFormatValue(value);
    }

    return super.formatValue(value);
  }

  stripPrefixSuffix(value) {
    if (typeof value === 'string') {
      try {
        const hasPrefix = this.currencyPrefix ? value.includes(this.currencyPrefix) : false;
        const hasSuffix = this.currencySuffix ? value.includes(this.currencySuffix) : false;
        const hasDelimiter = value.includes(this.delimiter);
        const hasDecimalSeparator = value.includes(this.decimalSeparator);

        if (this.currencyPrefix) {
          value = value.replace(this.currencyPrefix, '');
        }
        if (this.currencySuffix) {
          value = value.replace(this.currencySuffix, '');
        }
        //when we enter $ in the field using dashboard, it contains '_' that is NaN
        if ((hasPrefix || hasSuffix) && !hasDelimiter && !hasDecimalSeparator && (Number.isNaN(+value) || !value)) {
          value ='0';
        }
      }
      catch (err) {
        // If value doesn't have a replace method, continue on as before.
      }
    }
    return value;
  }

  addFocusBlurEvents(element) {
    super.addFocusBlurEvents(element);

    this.addEventListener(element, 'focus', () => {
      if (element.defaultValue === element.value) {
        element.setSelectionRange(0, element.defaultValue.length);
      }
    });
    this.addEventListener(element, 'blur', () => {
      element.value = this.getValueAsString(this.addZerosAndFormatValue(this.parseValue(element.value)));
    });
  }
}