Home Reference Source

src/utils/ChoicesWrapper.js

import Choices from '@formio/choices.js';

/**
 * TODO: REMOVE THIS ONCE THE PULL REQUEST HAS BEEN RESOLVED.
 *
 * https://github.com/jshjohnson/Choices/pull/788
 *
 * This is intentionally not part of the extended class, since other components use Choices and need this fix as well.
 * @type {Choices._generatePlaceholderValue}
 * @private
 */
Choices.prototype._generatePlaceholderValue = function() {
  if (this._isSelectElement && this.passedElement.placeholderOption) {
    const { placeholderOption } = this.passedElement;
    return placeholderOption ? placeholderOption.text : false;
  }
  const { placeholder, placeholderValue } = this.config;
  const {
    element: { dataset },
  } = this.passedElement;

  if (placeholder) {
    if (placeholderValue) {
      return placeholderValue;
    }

    if (dataset.placeholder) {
      return dataset.placeholder;
    }
  }

  return false;
};

export const KEY_CODES = {
  BACK_KEY: 46,
  DELETE_KEY: 8,
  TAB_KEY: 9,
  ENTER_KEY: 13,
  A_KEY: 65,
  ESC_KEY: 27,
  UP_KEY: 38,
  DOWN_KEY: 40,
  PAGE_UP_KEY: 33,
  PAGE_DOWN_KEY: 34,
};

class ChoicesWrapper extends Choices {
  constructor(...args) {
    super(...args);

    this._onTabKey = this._onTabKey.bind(this);
    this.isDirectionUsing = false;
    this.shouldOpenDropDown = true;
  }

  _handleButtonAction(activeItems, element) {
    if (!this._isSelectOneElement) {
      return super._handleButtonAction(activeItems, element);
    }

    if (
      !activeItems ||
      !element ||
      !this.config.removeItems ||
      !this.config.removeItemButton
    ) {
      return;
    }

    super._handleButtonAction(activeItems, element);
  }

  _onEnterKey(args) {
    // Prevent dropdown form opening when removeItemButton was pressed using 'Enter' on keyboard
    if (args.event.target.className === 'choices__button') {
      this.shouldOpenDropDown = false;
    }
    super._onEnterKey(args);
  }

  _onDirectionKey(...args) {
    if (!this._isSelectOneElement) {
      return super._onDirectionKey(...args);
    }

    this.isDirectionUsing = true;

    super._onDirectionKey(...args);

    this.onSelectValue(...args);

    clearTimeout(this.timeout);
    this.timeout = setTimeout(() => {
      this.isDirectionUsing = false;
    }, 250);
  }

  _onTabKey({ activeItems, hasActiveDropdown }) {
    if (hasActiveDropdown) {
      this._selectHighlightedChoice(activeItems);
    }
  }

  _selectHighlightedChoice(activeItems) {
    const highlightedChoice = this.dropdown.getChild(
      `.${this.config.classNames.highlightedState}`,
    );

    if (highlightedChoice) {
      this._handleChoiceAction(activeItems, highlightedChoice);
    }

    event.preventDefault();
  }

  _onKeyDown(event) {
    if (!this._isSelectOneElement) {
      return super._onKeyDown(event);
    }

    const { target, keyCode, ctrlKey, metaKey } = event;

    if (
      target !== this.input.element &&
      !this.containerOuter.element.contains(target)
    ) {
      return;
    }

    const activeItems = this._store.activeItems;
    const hasFocusedInput = this.input.isFocussed;
    const hasActiveDropdown = this.dropdown.isActive;
    const hasItems = this.itemList.hasChildren;
    const keyString = String.fromCharCode(keyCode);

    const {
      BACK_KEY,
      DELETE_KEY,
      TAB_KEY,
      ENTER_KEY,
      A_KEY,
      ESC_KEY,
      UP_KEY,
      DOWN_KEY,
      PAGE_UP_KEY,
      PAGE_DOWN_KEY,
    } = KEY_CODES;
    const hasCtrlDownKeyPressed = ctrlKey || metaKey;

    // If a user is typing and the dropdown is not active
    if (!hasActiveDropdown && !this._isTextElement && /[a-zA-Z0-9-_ ]/.test(keyString)) {
      const currentValue =  this.input.element.value;
      this.input.element.value = currentValue ? `${currentValue}${keyString}` : keyString;
      this.showDropdown();
    }

    // Map keys to key actions
    const keyDownActions = {
      [A_KEY]: this._onAKey,
      [TAB_KEY]: this._onTabKey,
      [ENTER_KEY]: this._onEnterKey,
      [ESC_KEY]: this._onEscapeKey,
      [UP_KEY]: this._onDirectionKey,
      [PAGE_UP_KEY]: this._onDirectionKey,
      [DOWN_KEY]: this._onDirectionKey,
      [PAGE_DOWN_KEY]: this._onDirectionKey,
      [DELETE_KEY]: this._onDeleteKey,
      [BACK_KEY]: this._onDeleteKey,
    };

    // If keycode has a function, run it
    if (keyDownActions[keyCode]) {
      keyDownActions[keyCode]({
        event,
        target,
        keyCode,
        metaKey,
        activeItems,
        hasFocusedInput,
        hasActiveDropdown,
        hasItems,
        hasCtrlDownKeyPressed,
      });
    }
  }

  onSelectValue({ event, activeItems, hasActiveDropdown }) {
    if (hasActiveDropdown) {
     this._selectHighlightedChoice(activeItems);
    }
    else if (this._isSelectOneElement) {
      this.showDropdown();
      event.preventDefault();
    }
  }

  showDropdown(...args) {
    if (!this.shouldOpenDropDown) {
      this.shouldOpenDropDown = true;
      return;
    }

    super.showDropdown(...args);
  }

  hideDropdown(...args) {
    if (this.isDirectionUsing) {
      return;
    }

    super.hideDropdown(...args);
  }

  _onBlur(...args) {
    if (this._isScrollingOnIe) {
      return;
    }
    super._onBlur(...args);
  }
}

export default ChoicesWrapper;