text: /**
   * @fileOverview
   * @name component<searchable-select>
   *
   * Fetures:
   * 1. options search/filter
   * 2. grouping
   * 3. show icons for unGroupedContent options
   * 4. `arrow-up` & `arrow-down` keys to navigate through options
   * 5. `return` key to select the current active option
   * 6. esc to cancel
   *
   * Option data structure:
   * {
   *  label: string,
   *  value: string,
   *  group: string,   // Optional, which group/category this option belong to.
   *  imgUrl: string,  // Optional, whether to display a image for this option, unGrouped options only.
   * }
   *
  **/
  import Component from '@ember/component';
  import { inject as service } from '@ember/service';
  import { next } from '@ember/runloop';
  import { get, set, computed, observer } from '@ember/object';
  import C from 'ui/utils/constants';
  import layout from './template';
  import { htmlSafe } from '@ember/string';
  import { on } from '@ember/object/evented';
  import { escapeRegex } from 'ui/utils/util';
  
  
  const MAX_HEIGHT = 285;
  
  export default Component.extend({
    intl:                service(),
  
    layout,
    classNames:          ['searchable-select'],
    classNameBindings:   ['class', 'showDropdownArrow'],
  
    // input
    class:               null,
    value:               null,
    prefix:              null,
    suffix:              null,
    prompt:              null,
    // If need to catch the group changes, you can pass a group prop in.
    group:               null,
    content:             null,
    interContent:        null,
    optionLabelPath:     'label',
    optionValuePath:     'value',
    optionGroupPath:     'group',
    localizedPrompt:     false,
    localizedLabel:      false,
    localizedHtmlLabel:  false,
    customLabel:         false,
    readOnly:            null,
  
    showOptions:         false,
    allowCustom:         false,
    filter:              null,
    clientSideFiltering: true,
    // the current highlighted option.
    $activeTarget:       null,
    maxHeight:           MAX_HEIGHT,
    showDropdownArrow:   true,
  
    init() {
      this._super(...arguments);
      this.observeContent();
    },
  
    didInsertElement() {
      this.$('.input-search').on('click', () => {
        this.send('show');
      });
    },
  
    willDestroyElement() {
      this.off();
    },
  
    actions: {
  
      search(/* term*/) {
        // placeholder is over written by extenders if you want
      },
  
      selectUnGroupedItem(idx) {
        const found = get(this, 'unGroupedContent').objectAt(idx);
  
        this.setSelect(found);
      },
  
      selectGroupedItem(items, idx) {
        const found = items.objectAt(idx);
  
        this.setSelect(found);
      },
  
      selectPrompt() {
        set(this, 'value', null);
  
        this.send('hide');
      },
  
      mouseEnter(event) {
        this.$('.searchable-option').removeClass('searchable-option-active');
  
        const $target = this.$(event.target);
  
        $target.addClass('searchable-option-active');
  
        set(this, '$activeTarget', $target);
      },
  
      mouseLeave(event) {
        this.$(event.target).removeClass('searchable-option-active');
  
        set(this, '$activeTarget', null);
      },
  
      show() {
        if (get(this, 'showOptions') === true) {
          return;
        }
  
        const toBottom = $('body').height() - $(this.$()[0]).offset().top - 60; // eslint-disable-line
  
        set(this, 'maxHeight', toBottom < MAX_HEIGHT ? toBottom : MAX_HEIGHT)
        set(this, 'filter', null);
  
        next(() => {
          const checked = this.$('.searchable-option .icon-check');
          const options = this.$('.searchable-options');
  
          if ( options.length && checked.length ) {
            options.animate({ scrollTop: `${ checked.parent().offset().top - options.offset().top }px` });
          }
        });
  
        set(this, 'showOptions', true);
      },
  
      hide() {
        set(this, 'filter', get(this, 'displayLabel'));
        set(this, 'showOptions', false);
        set(this, '$activeTarget', null);
      },
  
    },
  
    observeContent: observer('content.[]', 'value', 'displayLabel', function(){
      if (!get(this, 'content')) {
        set(this, 'content', []);
      }
  
      set(this, 'interContent', get(this, 'content').slice(0));
  
      if (get(this, 'allowCustom')) {
        set(this, 'searchLabel', 'generic.searchOrCustomInput');
  
        const value = get(this, 'value');
  
        this.insertCustomValue(value, false);
      }
  
      set(this, 'filter', get(this, 'displayLabel'));
    }),
  
    optionsMaxHeightCss: computed('maxHeight', function() {
      return htmlSafe(`max-height: ${  get(this, 'maxHeight')  }px`);
    }),
  
    // Show option image --> unGroupedContent only
    showOptionIcon: computed('unGroupedContent.@each.imgUrl', function() {
      return get(this, 'unGroupedContent').some((item) => !!item.imgUrl);
    }),
  
    displayLabel: computed('value', 'prompt', 'interContent.[]', 'intl.locale.[]', function() {
      const value = get(this, 'value');
      const vp = get(this, 'optionValuePath');
      const lp = get(this, 'optionLabelPath');
      const selectedItem = get(this, 'interContent').filterBy(vp, value).get('firstObject');
  
      if (selectedItem) {
        let label = get(selectedItem, lp);
  
        if (get(this, 'localizedLabel')) {
          label = get(this, 'intl').t(label);
        } else if ( get(this, 'localizedHtmlLabel') ) {
          label = value;
        }
  
        return label;
      }
  
      return null;
    }),
  
    filtered: computed('filter', 'interContent.[]', function() {
      const filter  = (get(this, 'filter') || '').trim();
      const options = get(this, 'interContent');
  
      if (get(this, 'allowCustom')) {
        this.insertCustomValue(filter, true);
      }
  
      if ( get(this, 'clientSideFiltering') ) {
        const filteredOptionsA = [];
        const filteredOptionsB = [];
  
        options.forEach((option) => {
          const filterTerms = filter.split(/\s+/);
          const gp          = get(this, 'optionGroupPath');
          const lp          = get(this, 'optionLabelPath');
          const group       = get(option, gp);
          const label       = get(option, lp);
  
          let startsWithOneOfFilterTerm = false;
          let containsEveryFilterTerm   = true;
  
          filterTerms.forEach((s) => {
            s = s.toLowerCase();
  
            const startsWith = label.toLowerCase().startsWith(s) || (group && group.toLowerCase().startsWith(s));
  
            if (startsWith) {
              startsWithOneOfFilterTerm = true;
            }
  
            const exp = escapeRegex(s);
  
            const pattern  = new RegExp(exp, 'i');
            const contains = pattern.test(label) || (group && pattern.test(group));
  
            if (!contains) {
              containsEveryFilterTerm = false;
            }
          });
  
          if (startsWithOneOfFilterTerm && containsEveryFilterTerm) {
            filteredOptionsA.push(option);
  
            return;
          }
  
          if (containsEveryFilterTerm) {
            filteredOptionsB.push(option);
  
            return;
          }
        });
  
        return filteredOptionsA.concat(filteredOptionsB);
      } else {
        return options;
      }
    }),
  
    unGroupedContent: computed('filtered.[]', function() {
      const groupPath = get(this, 'optionGroupPath');
      const out       = [];
  
      get(this, 'filtered').forEach((opt) => {
        const key = get(opt, groupPath);
  
        if (!key) {
          out.push(opt);
        }
      });
  
      return out;
    }),
  
    groupedContent: computed('filtered.[]', function() {
      const groupPath = get(this, 'optionGroupPath');
      const out       = [];
  
      get(this, 'filtered').forEach((opt) => {
        const key = get(opt, groupPath);
  
        if (key) {
          let group = out.filterBy('group', key)[0];
  
          if (!group) {
            group = {
              group:   key,
              options: []
            };
            out.push(group);
          }
  
          group.options.push(opt);
        }
      });
  
      return out.sortBy(groupPath);
    }),
  
    showMessage: computed('filtered.[]', function() {
      return get(this, 'filtered.length') === 0;
    }),
  
    missingMessage: computed('content.[]', function() {
      let len = get(this, 'content.length')
      let out = 'searchableSelect.noOptions';
  
      if (len) {
        out = 'searchableSelect.noMatch';
      }
  
      return out;
    }),
  
    showOptionsChanged: on('init', observer('showOptions', function() {
      const show = get(this, 'showOptions');
  
      if (show) {
        this.on();
      } else {
        this.off();
      }
    })),
  
    allContent() {
      const out       = [];
      const grouped   = get(this, 'groupedContent');
      const unGrouped = get(this, 'unGroupedContent');
  
      out.pushObjects(unGrouped);
  
      grouped.forEach((g) => out.pushObjects(g.options));
  
      return out;
    },
  
    on() {
      this.$().on('keydown.searchable-option', (event) => {
        const kc = event.keyCode;
  
        // Note: keyup event can't be prevented.
        if (!get(this, 'showOptions')) {
          return;
        }
  
        if (kc === C.KEY.UP) {
          this.stepThroughOptions(-1);
        }
  
        if (kc === C.KEY.DOWN) {
          this.stepThroughOptions(1);
        }
  
        // support using return key to select the current active option
        if (kc === C.KEY.CR || kc === C.KEY.LF) {
          event.preventDefault();
  
          const $activeTarget = get(this, '$activeTarget');
  
          if ($activeTarget) {
            // activeTarget is prompt
            if ($activeTarget.hasClass('searchable-prompt')) {
              this.send('selectPrompt');
            } else {
              let idx = this.$('.searchable-option').index($activeTarget);
  
              idx = !!get(this, 'prompt') ? idx - 1 : idx;
  
              // set value
              const activeOption = this.allContent().objectAt(idx);
  
              this.setSelect(activeOption);
            }
  
            // hide options after value has been set
            this.send('hide');
          }
        }
  
        // esc to hide
        if (kc === C.KEY.ESCAPE) {
          this.send('hide');
        }
      });
    },
  
    off() {
      if (this.$()) {
        this.$().off('keydown.searchable-option');
      }
    },
  
    setSelect(item) {
      const gp = get(this, 'optionGroupPath');
      const vp = get(this, 'optionValuePath');
  
      set(this, 'value', get(item, vp));
  
      if (gp && get(item, gp)) {
        set(this, 'group', get(item, gp));
      }
  
      set(this, 'filter', get(this, 'displayLabel'));
  
      // https://stackoverflow.com/questions/39624902/new-input-placeholder-behavior-in-safari-10-no-longer-hides-on-change-via-java
      next(() => {
        const input = this.$('.input-search');
  
        if ( input ) {
          input.focus();
          input.blur();
        }
      })
  
      this.sendAction('change', item);
      this.send('hide');
    },
  
    stepThroughOptions(step) {
      const $activeTarget = get(this, '$activeTarget');
      const $options      = this.$('.searchable-option');
      const len           = $options.length;
  
      let currentIdx      = -1;
      let nextIdx         = 0;
  
  
      if (len === 0) {
        return;
      }
  
      if (!$activeTarget) {
        $options.removeClass('searchable-option-active');
        $options.eq(0).addClass('searchable-option-active');
  
        set(this, '$activeTarget', $options.eq(0));
  
        return;
      }
  
      currentIdx = $options.index($activeTarget);
  
      if (currentIdx !== -1) {
        nextIdx = currentIdx + step;
      }
  
      if (nextIdx !== 0) {
        nextIdx = nextIdx < 0 ? len - 1 : nextIdx % len;
      }
  
      const $nextActiveTarget = $options.eq(nextIdx);
  
      set(this, '$activeTarget', $nextActiveTarget);
  
      $activeTarget.removeClass('searchable-option-active');
      $nextActiveTarget.addClass('searchable-option-active')
    },
  
    insertCustomValue(value, isFilter) {
      const vp = get(this, 'optionValuePath');
      const lp = get(this, 'optionLabelPath');
  
      value = value || '';
  
      if (!isFilter) {
        const custom = { custom: true, };
  
        custom[lp] = `${ value } (Custom)`;
        custom[vp] = value;
  
        get(this, 'interContent').pushObject(custom);
      } else {
        const found = get(this, 'interContent').filterBy('custom', true).get('firstObject');
  
        if (found) {
          set(found, lp, `${ value } (Custom)`);
          set(found, vp, value)
        }
      }
    },
  
  
  });
  