Home Reference Source

src/lib/services/Utilities.js

/**
 * Utility Service
 */
class Utilities {
  /**
   * Creates an instance of the Utility service
   */
  constructor(simpleKeyboardInstance){
    /**
     * @type {object} A simple-keyboard instance
     */
    this.simpleKeyboardInstance = simpleKeyboardInstance;

    /**
     * Bindings
     */
    this.getButtonClass = this.getButtonClass.bind(this);
    this.getButtonDisplayName = this.getButtonDisplayName.bind(this);
    this.getUpdatedInput = this.getUpdatedInput.bind(this);
    this.updateCaretPos = this.updateCaretPos.bind(this);
    this.isMaxLengthReached = this.isMaxLengthReached.bind(this);
    this.camelCase = this.camelCase.bind(this);
    this.countInArray = this.countInArray.bind(this);
  }

  /**
   * Adds default classes to a given button
   * 
   * @param  {string} button The button's layout name
   * @return {string} The classes to be added to the button
   */
  getButtonClass(button){
    let buttonTypeClass = (button.includes("{") && button.includes("}") && button !== '{//}') ? "functionBtn" : "standardBtn";
    let buttonWithoutBraces = button.replace("{", "").replace("}", "");
    let buttonNormalized = '';

    if(buttonTypeClass !== "standardBtn")
      buttonNormalized = ` hg-button-${buttonWithoutBraces}`;

    return `hg-${buttonTypeClass}${buttonNormalized}`;
  }

  /**
   * Default button display labels
   */
  getDefaultDiplay(){
    return {
      '{bksp}': 'backspace',
      '{backspace}': 'backspace',
      '{enter}': '< enter',
      '{shift}': 'shift',
      '{shiftleft}': 'shift',
      '{shiftright}': 'shift',
      '{alt}': 'alt',
      '{s}': 'shift',
      '{tab}': 'tab',
      '{lock}': 'caps',
      '{capslock}': 'caps',
      '{accept}': 'Submit',
      '{space}': ' ',
      '{//}': ' ',
      "{esc}": "esc",
      "{escape}": "esc",
      "{f1}": "f1",
      "{f2}": "f2",
      "{f3}": "f3",
      "{f4}": "f4",
      "{f5}": "f5",
      "{f6}": "f6",
      "{f7}": "f7",
      "{f8}": "f8",
      "{f9}": "f9",
      "{f10}": "f10",
      "{f11}": "f11",
      "{f12}": "f12",
      '{numpaddivide}': '/',
      '{numlock}': 'lock',
      "{arrowup}": "↑",
      "{arrowleft}": "←",
      "{arrowdown}": "↓",
      "{arrowright}": "→",
      "{prtscr}": "print",
      "{scrolllock}": "scroll",
      "{pause}": "pause",
      "{insert}": "ins",
      "{home}": "home",
      "{pageup}": "up",
      "{delete}": "del",
      "{end}": "end",
      "{pagedown}": "down",
      "{numpadmultiply}": "*",
      "{numpadsubtract}": "-",
      "{numpadadd}": "+",
      "{numpadenter}": "enter",
      "{period}": ".",
      "{numpaddecimal}": ".",
      "{numpad0}": "0",
      "{numpad1}": "1",
      "{numpad2}": "2",
      "{numpad3}": "3",
      "{numpad4}": "4",
      "{numpad5}": "5",
      "{numpad6}": "6",
      "{numpad7}": "7",
      "{numpad8}": "8",
      "{numpad9}": "9",
    };
  }
  /**
   * Returns the display (label) name for a given button
   * 
   * @param  {string} button The button's layout name
   * @param  {object} display The provided display option
   * @param  {boolean} mergeDisplay Whether the provided param value should be merged with the default one.
   */
  getButtonDisplayName(button, display, mergeDisplay){
    if(mergeDisplay){
      display = Object.assign({}, this.getDefaultDiplay(), display);
    } else {
      display = display || this.getDefaultDiplay();
    }

    return display[button] || button;
  }

  
  /**
   * Returns the updated input resulting from clicking a given button
   * 
   * @param  {string} button The button's layout name
   * @param  {string} input The input string
   * @param  {object} options The simple-keyboard options object
   * @param  {number} caretPos The cursor's current position
   */
  getUpdatedInput(button, input, options, caretPos){
    
    let output = input;

    if((button === "{bksp}" || button === "{backspace}") && output.length > 0){
      output = this.removeAt(output, caretPos);

    } else if(button === "{space}")
      output = this.addStringAt(output, " ", caretPos);

    else if(button === "{tab}" && !(typeof options.tabCharOnTab === "boolean" && options.tabCharOnTab === false)){
      output = this.addStringAt(output, "\t", caretPos);

    } else if((button === "{enter}" || button === "{numpadenter}") && options.newLineOnEnter)
      output = this.addStringAt(output, "\n", caretPos);

    else if(button.includes("numpad") && Number.isInteger(Number(button[button.length - 2]))){
      output = this.addStringAt(output, button[button.length - 2], caretPos);
    }
    else if(button === "{numpaddivide}")
      output = this.addStringAt(output, '/', caretPos);

    else if(button === "{numpadmultiply}")
      output = this.addStringAt(output, '*', caretPos);
    else if(button === "{numpadsubtract}")
      output = this.addStringAt(output, '-', caretPos);

    else if(button === "{numpadadd}")
      output = this.addStringAt(output, '+', caretPos);

    else if(button === "{numpaddecimal}")
      output = this.addStringAt(output, '.', caretPos);

    else if(button === "{" || button === "}")
      output = this.addStringAt(output, button, caretPos);

    else if(!button.includes("{") && !button.includes("}"))
      output = this.addStringAt(output, button, caretPos);

    return output;
  }
  /**
   * Moves the cursor position by a given amount
   * 
   * @param  {number} length Represents by how many characters the input should be moved
   * @param  {boolean} minus Whether the cursor should be moved to the left or not.
   */
  updateCaretPos(length, minus){
    if(minus){
      if(this.simpleKeyboardInstance.caretPosition > 0)
        this.simpleKeyboardInstance.caretPosition = this.simpleKeyboardInstance.caretPosition - length
    } else {
      this.simpleKeyboardInstance.caretPosition = this.simpleKeyboardInstance.caretPosition + length;
    }
  }

  /**
   * Adds a string to the input at a given position
   * 
   * @param  {string} source The source input
   * @param  {string} string The string to add
   * @param  {number} position The (cursor) position where the string should be added
   */
  addStringAt(source, string, position){
    let output;

    if(this.simpleKeyboardInstance.options.debug){
      console.log("Caret at:", position);
    }

    if(!position && position !== 0){
      output = source + string;
    } else {
      output = [source.slice(0, position), string, source.slice(position)].join('');

      /**
       * Avoid caret position change when maxLength is set
       */
      if(!this.isMaxLengthReached()){
        this.updateCaretPos(string.length);
      }

    }

    return output;
  }

  /**
   * Removes an amount of characters at a given position
   * 
   * @param  {string} source The source input
   * @param  {number} position The (cursor) position from where the characters should be removed
   */
  removeAt(source, position){
    if(this.simpleKeyboardInstance.caretPosition === 0){
      return source;
    }

    let output;
    let prevTwoChars;
    let emojiMatched;
    let emojiMatchedReg = /([\uD800-\uDBFF][\uDC00-\uDFFF])/g;

    /**
     * Emojis are made out of two characters, so we must take a custom approach to trim them.
     * For more info: https://mathiasbynens.be/notes/javascript-unicode
     */
    if(position && position >= 0){
      prevTwoChars = source.substring(position - 2, position)
      emojiMatched = prevTwoChars.match(emojiMatchedReg);

      if(emojiMatched){
        output = source.substr(0, (position - 2)) + source.substr(position);
        this.updateCaretPos(2, true);
      } else {
        output = source.substr(0, (position - 1)) + source.substr(position);
        this.updateCaretPos(1, true);
      }
    } else {
      prevTwoChars = source.slice(-2);
      emojiMatched = prevTwoChars.match(emojiMatchedReg);

      if(emojiMatched){
        output = source.slice(0, -2);
        this.updateCaretPos(2, true);
      } else {
        output = source.slice(0, -1);
        this.updateCaretPos(1, true);
      }
    }

    return output;
  }
  /**
   * Determines whether the maxLength has been reached. This function is called when the maxLength option it set.
   * 
   * @param  {object} inputObj
   * @param  {object} options
   * @param  {string} updatedInput
   */
  handleMaxLength(inputObj, options, updatedInput){
    let maxLength = options.maxLength;
    let currentInput = inputObj[options.inputName];
    let condition = currentInput.length === maxLength;


    if(
      /**
       * If pressing this button won't add more characters
       * We exit out of this limiter function
       */
      updatedInput.length <= currentInput.length
    ){
      return false;
    }

    if(Number.isInteger(maxLength)){
      if(options.debug){
        console.log("maxLength (num) reached:", condition);
      }

      if(condition){
        /**
         * @type {boolean} Boolean value that shows whether maxLength has been reached
         */
        this.maxLengthReached = true;
        return true;
      } else {
        this.maxLengthReached = false;
        return false;
      }
    }

    if(typeof maxLength === "object"){
      let condition = currentInput.length === maxLength[options.inputName];

      if(options.debug){
        console.log("maxLength (obj) reached:", condition);
      }

      if(condition){
        this.maxLengthReached = true;
        return true;
      } else {
        this.maxLengthReached = false;
        return false;
      }
    }
  }

  /**
   * Gets the current value of maxLengthReached
   */
  isMaxLengthReached(){
    return Boolean(this.maxLengthReached);
  }

  /**
   * Transforms an arbitrary string to camelCase
   * 
   * @param  {string} string The string to transform.
   */
  camelCase(string){
    return string.toLowerCase().trim().split(/[.\-_\s]/g).reduce((string, word) => string + word[0].toUpperCase() + word.slice(1));
  };

  /**
   * Counts the number of duplicates in a given array
   * 
   * @param  {Array} array The haystack to search in
   * @param  {string} value The needle to search for
   */
  countInArray(array, value){
    return array.reduce((n, x) => n + (x === value), 0);
  }

}

export default Utilities;