#152800. Make all reservations screens accessible for readers

<template>

    <btrz-label :id="id" :label="label"></btrz-label>
    <input class="form-control calendar-bkg"
    ref="input"
    :id="id"
    :name="name"
    :disabled="disabled"
    @input="valueUpdated($event.target.value);"
    @blur="focusUpdated('blur', $event.target.value);"
    @focus="focusUpdated('focus', $event.target.value);"/>
    {{displayDate}}
    <btrz-errors :errors="errors"></btrz-errors>

</template>

<template>
  <div :class="{
      'input--focused': focused,
      'input--empty': isEmpty,
      'input--filled': !isEmpty
    }">
    <btrz-label :id="id" :label="label"></btrz-label>
    <input class="form-control calendar-bkg"
    ref="input"
    :id="id"
    :name="name"
    :disabled="disabled"
    @input="valueUpdated($event.target.value);"
    @blur="focusUpdated('blur', $event.target.value);"
    @focus="focusUpdated('focus', $event.target.value);"/>
    <btrz-errors :errors="errors"></btrz-errors>
  </div>
</template>

<script>
  import moment from "moment";
  import BtrzLabel from "../../btrz-vue-components/src/btrz-label";
  import BtrzErrors from "../../btrz-vue-components/src/btrz-errors";

  const pickadateI18nStrings = {
    en: {
      monthsFull: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
      monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
      weekdaysFull: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
      weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
      // Buttons
      today: "Today",
      clear: "Clear",
      close: "Close",
      // Accessibility labels
      labelMonthNext: "Next month",
      labelMonthPrev: "Previous month",
      labelMonthSelect: "Select a month",
      labelYearSelect: "Select a year",
    },
    fr: {
      monthsFull: ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
      monthsShort: ["Jan", "Fev", "Mar", "Avr", "Mai", "Juin", "Juil", "Aou", "Sep", "Oct", "Nov", "Dec"],
      weekdaysFull: ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"],
      weekdaysShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"],
      // Buttons
      today: "Aujourd\"hui",
      clear: "Effacer",
      close: "Fermer",
      // Accessibility labels
      labelMonthNext: "Mois suivant",
      labelMonthPrev: "Mois précédent",
      labelMonthSelect: "Sélectionner un mois",
      labelYearSelect: "Sélectionner une année"
    },
    de: {
      monthsFull: ["August", "April", "Mai", "April", "Mai", "April", "Mai", "April", "Mai", "August"],
      monthsShort: ["Jan", "Fev", "Mar", "Avr", "Mai", "Juin", "Juil", "Aou", "Sep", "Oct", "Nov", "Dec"],
      weekdaysFull: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
      weekdaysShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"],
      // Buttons
      today: "Heute",
      clear: "Klar",
      close: "Schließen",
      // Accessibility labels
      labelMonthNext: "Nächsten Monat",
      labelMonthPrev: "Vorheriger Monat",
      labelMonthSelect: "Wähle einen Monat",
      labelYearSelect: "Wählen Sie ein Jahr aus"
    },
    nl: {
      monthsFull: ["Januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"],
      monthsShort: ["Jan", "Fev", "Mar", "Avr", "Mai", "Juin", "Juil", "Aou", "Sep", "Oct", "Nov", "Dec"],
      weekdaysFull: ["Zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"],
      weekdaysShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"],
      // Buttons
      today: "Vandaag",
      clear: "Duidelijk",
      close: "Dichtbij",
      // Accessibility labels
      labelMonthNext: "Mois suivant",
      labelMonthPrev: "Mois précédent",
      labelMonthSelect: "Sélectionner un mois",
      labelYearSelect: "Sélectionner une année"
    }
  };

  export default {
    name: "BtrzDateInput",
    components: {
      BtrzLabel,
      BtrzErrors
    },
    dependencies: ["logger"],
    props: {
      id: {type: String},
      placeholder: {type: String},
      name: {type: String},
      "value": {"default": null},
      // value: Supports setting Date, [YEAR, MONTH, DAY], timestamp (int) or string matching submitFormat
      displayFormat: {type: String, "default": "dd mmmm, yyyy"}, // Pickadate format
      submitFormat: {type: String, "default": "YYYY-MM-DD"}, // Momentjs format
      minDate: {"default": false},
      maxDate: {"default": false},
      lang: {type: String, required: true},
      disabled: {type: Boolean, "default": false},
      errors: {type: Object},
      label: {type: String}
    },
    model: {
      prop: "value",
      event: "change"
    },
    watch: {
      value(newValue) {
        this.setPickadateDate(newValue);
      },
      lang(newLang) {
        const translations = this.getTranslation(newLang);
        Object.assign(this.picker.component.settings, translations);
        this.picker.set("select", this.picker.get("select"), {muted: true});
        this.picker.render(true);
      },
      minDate(newMinDate) {
        this.setPickadateLimit(newMinDate, "min");
      },
      maxDate(newMaxDate) {
        this.setPickadateLimit(newMaxDate, "max");
      }
    },
    mounted() {
      require("imports-loader?define=>false!pickadate/lib/picker.date");
      require("pickadate/lib/themes/default.css");
      require("pickadate/lib/themes/default.date.css");

      const baseConfig = {
        onSet: this.onSet,
        format: this.displayFormat
      };

      const translations = this.getTranslation(this.lang);
      const config = Object.assign(baseConfig, translations);
      const $input = $(this.$refs.input).pickadate(config);
      this.picker = $input.pickadate("picker");

      $input.on('mousedown',function(event){
         event.preventDefault();
      })

      this.setPickadateDate(this.value);
      this.setPickadateLimit(this.minDate, "min");
      this.setPickadateLimit(this.maxDate, "max");
    },
    methods: {
      formatDate(date) {
        if (this.submitFormat) {
          return date ? moment(date).format(this.submitFormat) : date;
        } else {
          return date;
        }
      },
      onSet(context) {

        if ("select" in context) {
          if (typeof context.select === 'object' )
            context.select = context.select.pick;

          const date = this.formatDate(context.select);
          this.$emit("change", date);
          this.isEmpty = !Boolean(date);
        } else if ("clear" in context) {
          this.$emit("change", null);
          this.isEmpty = !Boolean(null);
        }
      },
      getTranslation(lang) {
        if (!(this.lang in pickadateI18nStrings)) {
          this.logger.warn(`Could not find the ${this.lang} language, defaulting to english...`);
          return pickadateI18nStrings.en;
        }
        return pickadateI18nStrings[lang];
      },
      setPickadateLimit(date, limit) {
        let parsedDate = null;

        if (date) {
          parsedDate = this.submitFormat ? moment(date, this.submitFormat).toDate() : date;
          const selectedDate = this.picker.get("select");
          if (selectedDate) {
            const isSelectedDateBeforeMinDate = (limit === "min" && moment(selectedDate.obj).isBefore(parsedDate, "days")),
              isSelectedDateAfterMaxDate = (limit === "max" && moment(selectedDate.obj).isAfter(parsedDate, "days"));

          // Clear value of input if the selected date is before the min date or after the max date
            if (isSelectedDateBeforeMinDate || isSelectedDateAfterMaxDate) {
              this.setPickadateDate(null);
              this.$emit("change", null);
            }
          }
        } else {
          parsedDate = false;
        }
        this.picker.set(limit, parsedDate);
      },
      setPickadateDate(newDate) {
        let date = null;

        if (typeof (newDate) === "string") {
          if (this.submitFormat) {
            date = moment(newDate, this.submitFormat);
            if (!date.isValid()) {
              throw new Error(date.toString());
            }
            date = date.toDate();
          } else {
            throw new Error("No submitFormat specified for this date");
          }
        } else if (newDate === null) {
          date = null;
        } else {
          date = moment(newDate).toDate();
        }
        this.picker.set("select", date, {muted: true});
      },
      valueUpdated(value) {
        this.inputValue = value;
      },
      focusUpdated(focus, value) {
        this.focused = !this.focused;
      }
    },
    data() {
      return {isEmpty: true, focused: false, inputValue: this.value};
    }
  };

</script>


Fixed image map component fit bounds logic for empty state

<script>
    import BasicMap from "./BasicMap";
    import {provideImageMarkerHtml} from "./imageMarkerProvider";

    export default {
        extends: BasicMap,
        props: {
            images: {
                type: Array,
                default() {
                    return [];
                },
            },
        },
        data() {
            return {
                markerGroup: null,
            };
        },
        watch: {
            images(images) {
                this.destroyMarkerCluster();
                this.initMarkerCluster();
                this.renderImages(images);
            },
        },
        methods: {
            initMarkerCluster() {
                this.markerGroup = L.markerClusterGroup({
                    spiderLegPolylineOptions: {opacity: 0},
                });
                this.map.addLayer(this.markerGroup);
            },
            destroyMarkerCluster() {
                if (this.markerGroup !== null) {
                    this.map.removeLayer(this.markerGroup);
                    this.markerGroup = null;
                }
            },
            renderImages(images) {
                images.forEach((image) => {
                    const icon = L.divIcon({html: provideImageMarkerHtml(image)});
                    const marker = L.marker(image.location, {icon, riseOnHover: true});
                    this.markerGroup.addLayer(marker);
                });

                const bounds = this.markerGroup.getBounds();
                if (bounds.isValid()) {
                    this.map.fitBounds(this.markerGroup.getBounds());
                }
            },
        },
        mounted() {
            this.initMarkerCluster();
        },
    }
</script>

<script>
    import BasicMap from "./BasicMap";
    import {provideImageMarkerHtml} from "./imageMarkerProvider";

    export default {
        extends: BasicMap,
        props: {
            images: {
                type: Array,
                default() {
                    return [];
                },
            },
        },
        data() {
            return {
                markerGroup: null,
            };
        },
        watch: {
            images(images) {
                this.destroyMarkerCluster();
                this.initMarkerCluster();
                this.renderImages(images);
            },
        },
        methods: {
            initMarkerCluster() {
                this.markerGroup = L.markerClusterGroup({
                    spiderLegPolylineOptions: {opacity: 0},
                });
                this.map.addLayer(this.markerGroup);
            },
            destroyMarkerCluster() {
                if (this.markerGroup !== null) {
                    this.map.removeLayer(this.markerGroup);
                    this.markerGroup = null;
                }
            },
            renderImages(images) {
                images.forEach((image) => {
                    const icon = L.divIcon({html: provideImageMarkerHtml(image)});
                    const marker = L.marker(image.location, {icon, riseOnHover: true});
                    this.markerGroup.addLayer(marker);
                });
                this.map.fitBounds(this.markerGroup.getBounds());
            },
        },
        mounted() {
            this.initMarkerCluster();
        },
    }
</script>


EBEAST: b/track-view.vue: use Util.shm_subscribe() for dB-meter updates

Signed-off-by: Tim Janik <6ec6ff229c4e1c5bbd5933ee92e6c5832ff023bb@gnu.org>

<docs>
  # B-TRACK-VIEW
  A Vue template to display a song's Bse.Track.
  ## Props:
  *project*
  : The *Bse.project* containing playback tracks.
  *track*
  : The *Bse.Track* to display.
</docs>

<template>

	{{ trackname }}
	<input v-if="nameedit_" v-inlineblur="() => nameedit_ = 0" :value="trackname"
	       type="text" @change="$event.target.cancelled || track.set_name ($event.target.value.trim())" />

       Track
            Add Track
         Rename Track
       Bounce Track
       Show / Hide Track Details

	      Cut
	     Copy
	    Paste

       Playback
           Mute Track
           Solo Track

       MIDI Channel
       Internal Channel

	  1
	  2
	  3
	  4

	  5
	  6
	  7
	  8

	  9
	 10
	 11
	 12

	 13
	 14
	 15
	 16

</template>


chore: use command key to select multiple files on mac (#1183)

<template>

      {{ icon }}

{{ name }}

—

{{ humanSize() }}

        <time :datetime="modified">{{ humanTime() }}</time>

</template>


Handle response from batch execute

Handle response from batch execute

<template>
  <div>
    <confirm-modal
      ref="confirmModal"
      :confirm-send-tx="sendTx"
      :signed-tx="signedTx"
      :fee="transactionFee"
      :is-hardware-wallet="isHardwareWallet"
      :gas-price="$store.state.gasPrice"
      :from="fromAddress"
      :to="toAddress"
      :value="amount"
      :gas="gasLimit"
      :data="data"
      :nonce="nonce"/>
    <confirm-collection-modal
      ref="confirmCollectionModal"
      :send-batch-transactions="sendBatchTransactions"
      :signed-array="signedArray"/>
    <confirm-modal
      ref="offlineGenerateConfirmModal"
      :confirm-send-tx="generateTx"
      :signed-tx="signedTx"
      :fee="transactionFee"
      :is-hardware-wallet="isHardwareWallet"
      :gas-price="$store.state.gasPrice"
      :from="fromAddress"
      :to="toAddress"
      :value="amount"
      :gas="gasLimit"
      :data="data"
      :nonce="nonce"/>
    <confirm-sign-modal
      ref="signConfirmModal"
      :confirm-sign-message="messageReturn"
      :show-success="showSuccessModal"
      :message-to-sign="messageToSign"
      :signed-message="signedMessage"
      :is-hardware-wallet="isHardwareWallet"
      :from="fromAddress"
    />
    <success-modal
      ref="successModal"
      :message="successMessage"
      :link-message="linkMessage"/>
  </div>
</template>

<script>
import * as unit from 'ethjs-unit';
import BN from 'bignumber.js';
import ConfirmModal from './components/ConfirmModal';
import ConfirmCollectionModal from './components/ConfirmCollectionModal';
import SuccessModal from './components/SuccessModal';
import ConfirmSignModal from './components/ConfirmSignModal';

export default {
  components: {
    'confirm-modal': ConfirmModal,
    'confirm-collection-modal': ConfirmCollectionModal,
    'success-modal': SuccessModal,
    'confirm-sign-modal': ConfirmSignModal
  },
  props: {
    active: {
      type: Boolean,
      default: false
    },
    rawTx: {
      type: Object,
      default: function() {
        return {};
      }
    }
  },
  data() {
    return {
      isHardwareWallet: false,
      responseFunction: null,
      advancedExpend: false,
      addressValid: true,
      amount: 0,
      amountValid: true,
      nonce: 0,
      gasLimit: 21000,
      data: '0x',
      gasAmount: this.$store.state.gasPrice,
      parsedBalance: 0,
      toAddress: '',
      transactionFee: 0,
      selectedCurrency: { symbol: 'ETH', name: 'Ethereum' },
      raw: {},
      ens: {},
      signer: {},
      signedTxObject: {},
      signedTx: '',
      messageToSign: '',
      signedMessage: '',
      successMessage: 'Success',
      linkMessage: 'OK',
      dismissed: true,
      web3WalletHash: '',
      web3WalletRes: '',
      signedArray: []
    };
  },
  computed: {
    fromAddress() {
      if (this.$store.state.wallet) {
        return this.$store.state.wallet.getChecksumAddressString();
      }
    }
  },
  watch: {
    web3WalletHash(newVal) {
      this.$store.dispatch('addNotification', [
        this.fromAddress,
        newVal,
        'Transaction Hash'
      ]);
      const pollReceipt = setInterval(() => {
        this.$store.state.web3.eth.getTransactionReceipt(newVal).then(res => {
          if (res !== null) {
            this.web3WalletRes = res;
            this.showSuccessModal('Transaction sent!', 'Okay');
            clearInterval(pollReceipt);
          }
        });
      }, 500);
    },
    web3WalletRes(newVal) {
      this.$store.dispatch('addNotification', [
        this.fromAddress,
        newVal,
        'Transaction Receipt'
      ]);
    }
  },
  created() {
    this.$eventHub.$on('showSuccessModal', (message, linkMessage) => {
      if (!message) message = null;
      this.showSuccessModal(message, linkMessage);
    });

    this.$eventHub.$on('showTxConfirmModal', (tx, resolve) => {
      this.parseRawTx(tx);
      if (tx.hasOwnProperty('ensObj')) {
        delete tx['ensObj'];
      }
      this.isHardwareWallet = this.$store.state.wallet.isHardware;
      this.responseFunction = resolve;
      this.successMessage = 'Sending Transaction';
      this.$store.state.wallet.signTransaction(tx).then(_response => {
        this.signedTxObject = _response;
        this.signedTx = this.signedTxObject.rawTransaction;
      });
      this.confirmationModalOpen();
    });

    this.$eventHub.$on('showWeb3Wallet', (tx, resolve) => {
      this.parseRawTx(tx);
      this.responseFunction = resolve;
      this.successMessage = 'Sending Transaction';
      this.$store.state.wallet.signTransaction(tx).then(_response => {
        this.web3WalletHash = _response;
      });
      this.showSuccessModal(
        'Continue transaction with Web3 Wallet Provider.',
        'Close'
      );
    });

    this.$eventHub.$on('showTxCollectionConfirmModal', (tx, isHardware) => {
      const newArr = [];
      this.isHardwareWallet = isHardware;
      for (let i = 0; i < tx.length; i++) {
        this.$store.state.wallet.signTransaction(tx[i]).then(_response => {
          newArr.push(_response);
        });
      }
      this.signedArray = newArr;
      this.confirmationCollectionModalOpen();
    });

    this.$eventHub.$on('showMessageConfirmModal', (data, resolve) => {
      this.responseFunction = resolve;
      this.messageToSign = data;
      this.$store.state.wallet.signMessage(data).then(_response => {
        this.signedMessage = '0x' + _response.toString('hex');
      });
      this.signConfirmationModalOpen();
    });
  },
  mounted() {
    this.$refs.confirmModal.$refs.confirmation.$on('hidden', () => {
      if (this.dismissed) {
        this.reset();
      }
    });

    this.$refs.successModal.$refs.success.$on('hide', () => {
      this.successMessage = '';
      this.linkMessage = 'OK';
    });
  },
  methods: {
    confirmationModalOpen() {
      window.scrollTo(0, 0);
      this.$refs.confirmModal.$refs.confirmation.show();
    },
    confirmationCollectionModalOpen() {
      window.scrollTo(0, 0);
      this.$refs.confirmCollectionModal.$refs.confirmCollection.show();
    },
    confirmationOfflineGenerateModalOpen() {
      window.scrollTo(0, 0);
      this.$refs.offlineGenerateConfirmModal.$refs.confirmation.show();
    },
    signConfirmationModalOpen() {
      window.scrollTo(0, 0);
      this.$refs.signConfirmModal.$refs.signConfirmation.show();
    },
    showSuccessModal(message, linkMessage) {
      this.reset();
      if (message !== null) this.successMessage = message;
      if (linkMessage !== null) this.linkMessage = linkMessage;
      this.$refs.successModal.$refs.success.show();
    },
    parseRawTx(tx) {
      this.raw = tx;
      this.nonce = +tx.nonce;
      this.data = tx.data;
      this.gasLimit = +tx.gas;
      this.toAddress = tx.to;
      this.amount = +tx.value;
      this.transactionFee = Number(
        unit.fromWei(new BN(tx.gasPrice).times(tx.gas).toString(), 'ether')
      );
      this.ens = {};
      if (tx.hasOwnProperty('ensObj')) {
        this.ens = Object.assign({}, tx.ensObj);
      }
    },
    messageReturn() {
      this.dismissed = false;
      this.responseFunction(this.signedMessage);
      this.$refs.signConfirmModal.$refs.signConfirmation.hide();
      this.showSuccessModal();
    },
    generateTx() {
      this.dismissed = false;
      this.responseFunction(this.signedTxObject);
      this.$refs.confirmModal.$refs.confirmation.hide();
    },
    async sendBatchCallback(err, response) {
      if (err !== null) {
        this.$store.dispatch('addNotification', [
          this.fromAddress,
          err,
          'Transaction Error'
        ]);
        return;
      }

      this.$store.dispatch('addNotification', [
        this.fromAddress,
        response,
        'Transaction Hash'
      ]);

      const pollReceipt = setInterval(() => {
        this.$store.state.web3.eth.getTransactionReceipt(response).then(res => {
          if (res !== null) {
            this.$store.dispatch('addNotification', [
              this.fromAddress,
              res,
              'Transaction Receipt'
            ]);
            this.showSuccessModal('Transaction sent!', 'Okay');
            clearInterval(pollReceipt);
          }
        });
      }, 500);
    },
    async sendBatchTransactions() {
      const web3 = this.$store.state.web3;
      const batch = new web3.eth.BatchRequest();
      for (let i = 0; i < this.signedArray.length; i++) {
        batch.add(
          web3.eth.sendSignedTransaction.request(
            this.signedArray[i].rawTransaction,
            this.sendBatchCallback
          )
        );
      }
      batch.execute();
    },
    sendTx() {
      this.dismissed = false;
      this.responseFunction(this.signedTxObject);
      this.$refs.confirmModal.$refs.confirmation.hide();
      this.showSuccessModal();
    },
    reset() {
      this.responseFunction = null;
      this.advancedExpend = false;
      this.addressValid = true;
      this.amount = 0;
      this.amountValid = true;
      this.nonce = 0;
      this.gasLimit = 21000;
      this.data = '0x';
      this.gasAmount = this.$store.state.gasPrice;
      this.parsedBalance = 0;
      this.toAddress = '';
      this.transactionFee = 0;
      this.selectedCurrency = { symbol: 'ETH', name: 'Ethereum' };
      this.raw = {};
      this.signedTx = '';
      this.messageToSign = '';
      this.signedMessage = '';
      this.messageToSign = '';
      this.signedArray = [];
      this.web3WalletHash = '';
      this.web3WalletRes = '';
    }
  }
};
</script>

<style scoped>
</style>

<template>
  <div>
    <confirm-modal
      ref="confirmModal"
      :confirm-send-tx="sendTx"
      :signed-tx="signedTx"
      :fee="transactionFee"
      :is-hardware-wallet="isHardwareWallet"
      :gas-price="$store.state.gasPrice"
      :from="fromAddress"
      :to="toAddress"
      :value="amount"
      :gas="gasLimit"
      :data="data"
      :nonce="nonce"/>
    <confirm-collection-modal
      ref="confirmCollectionModal"
      :send-batch-transactions="sendBatchTransactions"
      :signed-array="signedArray"/>
    <confirm-modal
      ref="offlineGenerateConfirmModal"
      :confirm-send-tx="generateTx"
      :signed-tx="signedTx"
      :fee="transactionFee"
      :is-hardware-wallet="isHardwareWallet"
      :gas-price="$store.state.gasPrice"
      :from="fromAddress"
      :to="toAddress"
      :value="amount"
      :gas="gasLimit"
      :data="data"
      :nonce="nonce"/>
    <confirm-sign-modal
      ref="signConfirmModal"
      :confirm-sign-message="messageReturn"
      :show-success="showSuccessModal"
      :message-to-sign="messageToSign"
      :signed-message="signedMessage"
      :is-hardware-wallet="isHardwareWallet"
      :from="fromAddress"
    />
    <success-modal
      ref="successModal"
      :message="successMessage"
      :link-message="linkMessage"/>
  </div>
</template>

<script>
import * as unit from 'ethjs-unit';
import BN from 'bignumber.js';
import ConfirmModal from './components/ConfirmModal';
import ConfirmCollectionModal from './components/ConfirmCollectionModal';
import SuccessModal from './components/SuccessModal';
import ConfirmSignModal from './components/ConfirmSignModal';

export default {
  components: {
    'confirm-modal': ConfirmModal,
    'confirm-collection-modal': ConfirmCollectionModal,
    'success-modal': SuccessModal,
    'confirm-sign-modal': ConfirmSignModal
  },
  props: {
    active: {
      type: Boolean,
      default: false
    },
    rawTx: {
      type: Object,
      default: function() {
        return {};
      }
    }
  },
  data() {
    return {
      isHardwareWallet: false,
      responseFunction: null,
      advancedExpend: false,
      addressValid: true,
      amount: 0,
      amountValid: true,
      nonce: 0,
      gasLimit: 21000,
      data: '0x',
      gasAmount: this.$store.state.gasPrice,
      parsedBalance: 0,
      toAddress: '',
      transactionFee: 0,
      selectedCurrency: { symbol: 'ETH', name: 'Ethereum' },
      raw: {},
      ens: {},
      signer: {},
      signedTxObject: {},
      signedTx: '',
      messageToSign: '',
      signedMessage: '',
      successMessage: 'Success',
      linkMessage: 'OK',
      dismissed: true,
      web3WalletHash: '',
      web3WalletRes: '',
      signedArray: []
    };
  },
  computed: {
    fromAddress() {
      if (this.$store.state.wallet) {
        return this.$store.state.wallet.getAddressString();
      }
    }
  },
  watch: {
    web3WalletHash(newVal) {
      this.$store.dispatch('addNotification', [
        this.fromAddress,
        newVal,
        'Transaction Hash'
      ]);
      const pollReceipt = setInterval(() => {
        this.$store.state.web3.eth.getTransactionReceipt(newVal).then(res => {
          if (res !== null) {
            this.web3WalletRes = res;
            this.showSuccessModal('Transaction sent!', 'Okay');
            clearInterval(pollReceipt);
          }
        });
      }, 500);
    },
    web3WalletRes(newVal) {
      this.$store.dispatch('addNotification', [
        this.fromAddress,
        newVal,
        'Transaction Receipt'
      ]);
    }
  },
  created() {
    this.$eventHub.$on('showSuccessModal', (message, linkMessage) => {
      if (!message) message = null;
      this.showSuccessModal(message, linkMessage);
    });

    this.$eventHub.$on('showTxConfirmModal', (tx, resolve) => {
      this.parseRawTx(tx);
      if (tx.hasOwnProperty('ensObj')) {
        delete tx['ensObj'];
      }
      this.isHardwareWallet = this.$store.state.wallet.isHardware;
      this.responseFunction = resolve;
      this.successMessage = 'Sending Transaction';
      this.$store.state.wallet.signTransaction(tx).then(_response => {
        this.signedTxObject = _response;
        this.signedTx = this.signedTxObject.rawTransaction;
      });
      this.confirmationModalOpen();
    });

    this.$eventHub.$on('showWeb3Wallet', (tx, resolve) => {
      this.parseRawTx(tx);
      this.responseFunction = resolve;
      this.successMessage = 'Sending Transaction';
      this.$store.state.wallet.signTransaction(tx).then(_response => {
        this.web3WalletHash = _response;
      });
      this.showSuccessModal(
        'Continue transaction with Web3 Wallet Provider.',
        'Close'
      );
    });

    this.$eventHub.$on('showTxCollectionConfirmModal', (tx, isHardware) => {
      const newArr = [];
      this.isHardwareWallet = isHardware;
      for (let i = 0; i < tx.length; i++) {
        this.$store.state.wallet.signTransaction(tx[i]).then(_response => {
          newArr.push(_response);
        });
      }
      this.signedArray = newArr;
      this.confirmationCollectionModalOpen();
    });

    this.$eventHub.$on('showMessageConfirmModal', (data, resolve) => {
      this.responseFunction = resolve;
      this.messageToSign = data;
      this.$store.state.wallet.signMessage(data).then(_response => {
        this.signedMessage = '0x' + _response.toString('hex');
      });
      this.signConfirmationModalOpen();
    });
  },
  mounted() {
    this.$refs.confirmModal.$refs.confirmation.$on('hidden', () => {
      if (this.dismissed) {
        this.reset();
      }
    });

    this.$refs.successModal.$refs.success.$on('hide', () => {
      this.successMessage = '';
      this.linkMessage = 'OK';
    });
  },
  methods: {
    confirmationModalOpen() {
      window.scrollTo(0, 0);
      this.$refs.confirmModal.$refs.confirmation.show();
    },
    confirmationCollectionModalOpen() {
      window.scrollTo(0, 0);
      this.$refs.confirmCollectionModal.$refs.confirmCollection.show();
    },
    confirmationOfflineGenerateModalOpen() {
      window.scrollTo(0, 0);
      this.$refs.offlineGenerateConfirmModal.$refs.confirmation.show();
    },
    signConfirmationModalOpen() {
      window.scrollTo(0, 0);
      this.$refs.signConfirmModal.$refs.signConfirmation.show();
    },
    showSuccessModal(message, linkMessage) {
      this.reset();
      if (message !== null) this.successMessage = message;
      if (linkMessage !== null) this.linkMessage = linkMessage;
      this.$refs.successModal.$refs.success.show();
    },
    parseRawTx(tx) {
      this.raw = tx;
      this.nonce = +tx.nonce;
      this.data = tx.data;
      this.gasLimit = +tx.gas;
      this.toAddress = tx.to;
      this.amount = +tx.value;
      this.transactionFee = Number(
        unit.fromWei(new BN(tx.gasPrice).times(tx.gas).toString(), 'ether')
      );
      this.ens = {};
      if (tx.hasOwnProperty('ensObj')) {
        this.ens = Object.assign({}, tx.ensObj);
      }
    },
    messageReturn() {
      this.dismissed = false;
      this.responseFunction(this.signedMessage);
      this.$refs.signConfirmModal.$refs.signConfirmation.hide();
      this.showSuccessModal();
    },
    generateTx() {
      this.dismissed = false;
      this.responseFunction(this.signedTxObject);
      this.$refs.confirmModal.$refs.confirmation.hide();
    },
    sendBatchCallback(x) {
      console.log(x);
    },
    async sendBatchTransactions() {
      const web3 = this.$store.state.web3;
      const batch = new web3.eth.BatchRequest();
      for (let i = 0; i < this.signedArray.length; i++) {
        batch.add(
          web3.eth.sendSignedTransaction.request(
            this.signedArray[i].rawTransaction
          ),
          this.sendBatchCallback
        );
      }
      batch.execute();
    },
    sendTx() {
      this.dismissed = false;
      this.responseFunction(this.signedTxObject);
      this.$refs.confirmModal.$refs.confirmation.hide();
      this.showSuccessModal();
    },
    reset() {
      this.responseFunction = null;
      this.advancedExpend = false;
      this.addressValid = true;
      this.amount = 0;
      this.amountValid = true;
      this.nonce = 0;
      this.gasLimit = 21000;
      this.data = '0x';
      this.gasAmount = this.$store.state.gasPrice;
      this.parsedBalance = 0;
      this.toAddress = '';
      this.transactionFee = 0;
      this.selectedCurrency = { symbol: 'ETH', name: 'Ethereum' };
      this.raw = {};
      this.signedTx = '';
      this.messageToSign = '';
      this.signedMessage = '';
      this.messageToSign = '';
      this.signedArray = [];
      this.web3WalletHash = '';
      this.web3WalletRes = '';
    }
  }
};
</script>

<style scoped>
</style>


[FrontEnd] update

[FrontEnd] update

<template>

            个人中心

用户信息

更改密码

            活动

已加入活动

参加过的活动

            社团

已加入社团

          <el-form ref="ProfileForm" :model="ProfileForm">
            <el-form-item prop="UserName" label="用户名" :required="true">
              <el-input v-model="ProfileForm.UserName"></el-input>
            </el-form-item>
            <el-form-item prop="Grade" label="年级" :required="true">
              <el-select v-model="ProfileForm.Grade" value="">
                <el-option label="高一" value="1"></el-option>
                <el-option label="高二" value="2"></el-option>
                <el-option label="高三" value="3"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item prop="Class" label="班级" :required="true">
              <el-select v-model="ProfileForm.Class" value="">
                <el-option label="1" value="1"></el-option>
                <el-option label="2" value="2"></el-option>
                <el-option label="3" value="3"></el-option>
                <el-option label="4" value="4"></el-option>
                <el-option label="5" value="5"></el-option>
                <el-option label="6" value="6"></el-option>
                <el-option label="7" value="7"></el-option>
                <el-option label="8" value="8"></el-option>
                <el-option label="9" value="9"></el-option>
                <el-option label="10" value="10"></el-option>
                <el-option label="11" value="11"></el-option>
                <el-option label="12" value="12"></el-option>
                <el-option label="13" value="13"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item prop="AttendYear" label="入学年份" :required="true">
              <el-input v-model="ProfileForm.AttendYear"></el-input>
            </el-form-item>
            <el-form-item label="QQ" prop="QQ">
              <el-input v-model="ProfileForm.QQ"></el-input>
            </el-form-item>
            <el-form-item label="电子邮箱" prop="Email">
              <el-input v-model="ProfileForm.Email"></el-input>
            </el-form-item>
            <el-form-item label="手机号码" prop="Phone">
              <el-input v-model="ProfileForm.Phone"></el-input>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="onSubmit()">更改</el-button>
              <el-button>取消</el-button>
            </el-form-item>
          </el-form>

不准改

已加入活动

参加过的活动

已加入社团

{{ club }}

                  <button class="btn btn-danger" type="button" @click="quitClub(club.id)">我要退出</button>

</template>

<template>
  <el-row>
    <el-col :span="4">
      <el-menu mode="vertical" class="el-menu-vertical-demo" :default-active=activeIndex @select="handleSelect">
        <el-menu-item-group title="个人中心">
          <el-menu-item index="1">用户信息</el-menu-item>
          <el-menu-item index="2">更改密码</el-menu-item>
        </el-menu-item-group>
        <el-menu-item-group title="活动">
          <el-menu-item index="3">已加入活动</el-menu-item>
          <el-menu-item index="4">参加过的活动</el-menu-item>
        </el-menu-item-group>
        <el-menu-item-group title="社团">
          <el-menu-item index="5">已加入社团</el-menu-item>
        </el-menu-item-group>
      </el-menu>
    </el-col>
    <el-col :span="20">

        <el-col :span="8" :offset="2">
          <el-form ref="ProfileForm" :model="ProfileForm">
            <el-form-item prop="UserName" label="用户名" :required="true">
              <el-input v-model="ProfileForm.UserName"></el-input>
            </el-form-item>
            <el-form-item prop="Grade" label="年级" :required="true">
              <el-select v-model="ProfileForm.Grade" value="">
                <el-option label="高一" value="1"></el-option>
                <el-option label="高二" value="2"></el-option>
                <el-option label="高三" value="3"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item prop="Class" label="班级" :required="true">
              <el-select v-model="ProfileForm.Class" value="">
                <el-option label="1" value="1"></el-option>
                <el-option label="2" value="2"></el-option>
                <el-option label="3" value="3"></el-option>
                <el-option label="4" value="4"></el-option>
                <el-option label="5" value="5"></el-option>
                <el-option label="6" value="6"></el-option>
                <el-option label="7" value="7"></el-option>
                <el-option label="8" value="8"></el-option>
                <el-option label="9" value="9"></el-option>
                <el-option label="10" value="10"></el-option>
                <el-option label="11" value="11"></el-option>
                <el-option label="12" value="12"></el-option>
                <el-option label="13" value="13"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item prop="AttendYear" label="入学年份" :required="true">
              <el-input v-model="ProfileForm.AttendYear"></el-input>
            </el-form-item>
            <el-form-item label="QQ" prop="QQ">
              <el-input v-model="ProfileForm.QQ"></el-input>
            </el-form-item>
            <el-form-item label="电子邮箱" prop="Email">
              <el-input v-model="ProfileForm.Email"></el-input>
            </el-form-item>
            <el-form-item label="手机号码" prop="Phone">
              <el-input v-model="ProfileForm.Phone"></el-input>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="onSubmit()">更改</el-button>
              <el-button>取消</el-button>
            </el-form-item>
          </el-form>
        </el-col>

不准改

已加入活动

参加过的活动

已加入社团

{{ club }}

                <button class="btn btn-danger" type="button" @click="quitClub(club.id)">我要退出</button>

    </el-col>
  </el-row>
</template>


Add html5 option to KIcon

Add html5 option to KIcon

<template>

  <!-- eslint-disable max-len -->

  <!-- tracking -->
  <mat-svg v-if="correct" name="check_circle" category="action" :style="style" />
  <mat-svg v-else-if="helpNeeded" name="error" category="alert" :style="style" />
  <mat-svg v-else-if="hint" name="lightbulb_outline" category="action" :style="style" />
  <mat-svg v-else-if="incorrect" name="close" category="navigation" :style="style" />
  <mat-svg v-else-if="inProgress" name="access_time" category="device" :style="style" />
  <mat-svg v-else-if="mastered" name="stars" category="action" :style="style" />
  <mat-svg v-else-if="notStarted" name="brightness_1" category="image" :style="style" />
  <mat-svg v-else-if="rectified" name="lens" category="image" :style="style" />
  <!-- coaching -->
  <mat-svg v-else-if="coach" name="local_library" category="maps" :style="style" />
  <mat-svg v-else-if="lesson" name="import_contacts" category="communication" :style="style" />
  <mat-svg v-else-if="question" name="keyboard_arrow_right" category="hardware" :style="style" :class="flip" />
  <mat-svg v-else-if="quiz" name="assignment_late" category="action" :style="style" />
  <!-- content -->
  <mat-svg v-else-if="app" name="widgets" category="device" :style="style" />
  <mat-svg v-else-if="audio" name="audiotrack" category="image" :style="style" />
  <mat-svg v-else-if="channel" name="apps" category="navigation" :style="style" />
  <mat-svg v-else-if="doc" name="book" category="action" :style="style" />
  <mat-svg v-else-if="exercise" name="assignment" category="action" :style="style" />
  <mat-svg v-else-if="topic" name="folder" category="file" :style="style" />
  <mat-svg v-else-if="video" name="ondemand_video" category="notification" :style="style" />
  <mat-svg v-else-if="html5" name="widgets" category="device" :style="style" />
  <!-- users -->
  <mat-svg v-else-if="classroom" name="business" category="communication" :style="style" />
  <mat-svg v-else-if="group" category="action" name="group_work" :style="style" />
  <mat-svg v-else-if="people" name="people" category="social" :style="style" />
  <mat-svg v-else-if="person" name="person" category="social" :style="style" />
  <mat-svg v-else-if="permission" name="vpn_key" category="communication" :style="style" />
  <!-- misc -->
  <mat-svg v-else-if="dot" name="brightness_1" category="image" :style="style" />
  <mat-svg v-else-if="error" name="error" category="alert" :style="style" />
  <!-- UI -->
  <mat-svg v-else-if="back" name="arrow_back" category="navigation" :style="style" :class="flip" />
  <mat-svg v-else-if="forward" name="arrow_forward" category="navigation" :style="style" :class="flip" />
  <mat-svg v-else-if="clear" name="clear" category="content" :style="style" />
  <mat-svg v-else-if="dropdown" name="arrow_drop_down" category="navigation" :style="style" />
  <mat-svg v-else-if="language" name="language" category="action" :style="style" />
  <mat-svg v-else-if="logout" name="exit_to_app" category="action" :style="style" :class="flip" />
  <mat-svg v-else-if="menu" name="menu" category="navigation" :style="style" />
  <mat-svg v-else-if="search" name="search" category="action" :style="style" />

  <!--eslint-enable-->

</template>

<script>

  import themeMixin from 'kolibri.coreVue.mixins.themeMixin';

  const boolean = {
    type: Boolean,
    default: false,
  };

  export default {
    name: 'KIcon',
    mixins: [themeMixin],
    props: {
      /**
       * color to apply to the icon
       */
      color: {
        type: String,
        required: false,
      },
      // tracking
      correct: boolean,
      helpNeeded: boolean,
      hint: boolean,
      incorrect: boolean,
      inProgress: boolean,
      mastered: boolean,
      notStarted: boolean,
      rectified: boolean,
      // coaching
      coach: boolean,
      lesson: boolean,
      question: boolean,
      quiz: boolean,
      // content
      app: boolean,
      audio: boolean,
      channel: boolean,
      doc: boolean,
      exercise: boolean,
      topic: boolean,
      video: boolean,
      html5: boolean,
      // users
      classroom: boolean,
      group: boolean,
      people: boolean,
      permission: boolean,
      person: boolean,
      // misc
      dot: boolean,
      error: boolean,
      // UI
      back: boolean,
      forward: boolean,
      clear: boolean,
      dropdown: boolean,
      language: boolean,
      logout: boolean,
      menu: boolean,
      search: boolean,
    },
    computed: {
      style() {
        if (this.color) {
          return { fill: this.color };
        }
        return { fill: this.$themeTokens.text };
      },
      flip() {
        return this.isRtl ? 'rtl-icon' : null;
      },
    },
  };

</script>

<style lang="scss" scoped>

  svg {
    position: relative;
    top: 0.125em;
    width: 1.125em;
    height: 1.125em;
  }

</style>

<template>

  <!-- eslint-disable max-len -->

  <!-- tracking -->
  <mat-svg v-if="correct" name="check_circle" category="action" :style="style" />
  <mat-svg v-else-if="helpNeeded" name="error" category="alert" :style="style" />
  <mat-svg v-else-if="hint" name="lightbulb_outline" category="action" :style="style" />
  <mat-svg v-else-if="incorrect" name="close" category="navigation" :style="style" />
  <mat-svg v-else-if="inProgress" name="access_time" category="device" :style="style" />
  <mat-svg v-else-if="mastered" name="stars" category="action" :style="style" />
  <mat-svg v-else-if="notStarted" name="brightness_1" category="image" :style="style" />
  <mat-svg v-else-if="rectified" name="lens" category="image" :style="style" />
  <!-- coaching -->
  <mat-svg v-else-if="coach" name="local_library" category="maps" :style="style" />
  <mat-svg v-else-if="lesson" name="import_contacts" category="communication" :style="style" />
  <mat-svg v-else-if="question" name="keyboard_arrow_right" category="hardware" :style="style" :class="flip" />
  <mat-svg v-else-if="quiz" name="assignment_late" category="action" :style="style" />
  <!-- content -->
  <mat-svg v-else-if="app" name="widgets" category="device" :style="style" />
  <mat-svg v-else-if="audio" name="audiotrack" category="image" :style="style" />
  <mat-svg v-else-if="channel" name="apps" category="navigation" :style="style" />
  <mat-svg v-else-if="doc" name="book" category="action" :style="style" />
  <mat-svg v-else-if="exercise" name="assignment" category="action" :style="style" />
  <mat-svg v-else-if="topic" name="folder" category="file" :style="style" />
  <mat-svg v-else-if="video" name="ondemand_video" category="notification" :style="style" />
  <!-- users -->
  <mat-svg v-else-if="classroom" name="business" category="communication" :style="style" />
  <mat-svg v-else-if="group" category="action" name="group_work" :style="style" />
  <mat-svg v-else-if="people" name="people" category="social" :style="style" />
  <mat-svg v-else-if="person" name="person" category="social" :style="style" />
  <mat-svg v-else-if="permission" name="vpn_key" category="communication" :style="style" />
  <!-- misc -->
  <mat-svg v-else-if="dot" name="brightness_1" category="image" :style="style" />
  <mat-svg v-else-if="error" name="error" category="alert" :style="style" />
  <!-- UI -->
  <mat-svg v-else-if="back" name="arrow_back" category="navigation" :style="style" :class="flip" />
  <mat-svg v-else-if="forward" name="arrow_forward" category="navigation" :style="style" :class="flip" />
  <mat-svg v-else-if="clear" name="clear" category="content" :style="style" />
  <mat-svg v-else-if="dropdown" name="arrow_drop_down" category="navigation" :style="style" />
  <mat-svg v-else-if="language" name="language" category="action" :style="style" />
  <mat-svg v-else-if="logout" name="exit_to_app" category="action" :style="style" :class="flip" />
  <mat-svg v-else-if="menu" name="menu" category="navigation" :style="style" />
  <mat-svg v-else-if="search" name="search" category="action" :style="style" />

  <!--eslint-enable-->

</template>

<script>

  import themeMixin from 'kolibri.coreVue.mixins.themeMixin';

  const boolean = {
    type: Boolean,
    default: false,
  };

  export default {
    name: 'KIcon',
    mixins: [themeMixin],
    props: {
      /**
       * color to apply to the icon
       */
      color: {
        type: String,
        required: false,
      },
      // tracking
      correct: boolean,
      helpNeeded: boolean,
      hint: boolean,
      incorrect: boolean,
      inProgress: boolean,
      mastered: boolean,
      notStarted: boolean,
      rectified: boolean,
      // coaching
      coach: boolean,
      lesson: boolean,
      question: boolean,
      quiz: boolean,
      // content
      app: boolean,
      audio: boolean,
      channel: boolean,
      doc: boolean,
      exercise: boolean,
      topic: boolean,
      video: boolean,
      // users
      classroom: boolean,
      group: boolean,
      people: boolean,
      permission: boolean,
      person: boolean,
      // misc
      dot: boolean,
      error: boolean,
      // UI
      back: boolean,
      forward: boolean,
      clear: boolean,
      dropdown: boolean,
      language: boolean,
      logout: boolean,
      menu: boolean,
      search: boolean,
    },
    computed: {
      style() {
        if (this.color) {
          return { fill: this.color };
        }
        return { fill: this.$themeTokens.text };
      },
      flip() {
        return this.isRtl ? 'rtl-icon' : null;
      },
    },
  };

</script>

<style lang="scss" scoped>

  svg {
    position: relative;
    top: 0.125em;
    width: 1.125em;
    height: 1.125em;
  }

</style>


Cleaned Up Flash Method

Cleaned Up Flash Method

<template>

        <transition-group :name="transition" tag="div">

                {{ item.type }}:

                {{ item.message }}

        </transition-group>

</template>


fix(inputProps) use internal inputProps (#130)

<template>
  <div :id="componentAttrIdAutosuggest">
    <slot name="before-input" /><input
      :type="internal_inputProps.type"
      :value="internalValue"
      :autocomplete="internal_inputProps.autocomplete"
      role="combobox"
      :class="[isOpen ? 'autosuggest__input--open' : '', internal_inputProps['class']]"
      v-bind="internal_inputProps"
      aria-autocomplete="list"
      aria-owns="autosuggest__results"
      :aria-activedescendant="isOpen && currentIndex !== null ? `autosuggest__results-item--${currentIndex}` : ''"
      :aria-haspopup="isOpen ? 'true' : 'false'"
      :aria-expanded="isOpen ? 'true' : 'false'"
      @input="inputHandler"
      @keydown="handleKeyStroke"
      v-on="listeners"
    ><slot name="after-input" />
    <div :class="componentAttrClassAutosuggestResultsContainer">
      <div
        v-if="isOpen"
        :class="componentAttrClassAutosuggestResults"
        :aria-labelledby="componentAttrIdAutosuggest"
      >
        <slot name="before-suggestions" />
        <component
          :is="cs.type"
          v-for="(cs, key) in computedSections"
          :ref="getSectionRef(key)"
          :key="getSectionRef(key)"
          :current-index="currentIndex"
          :normalize-item-function="normalizeItem"
          :render-suggestion="renderSuggestion"
          :section="cs"
          @updateCurrentIndex="updateCurrentIndex"
        >
          <template
            :slot="`before-section-${cs.name || cs.label}`"
            slot-scope="{section, className}"
          >
            <slot
              :name="`before-section-${cs.name || cs.label}`"
              :section="section"
              :className="className"
            />
          </template>
          <template slot-scope="{ suggestion, _key }">
            <slot
              :suggestion="suggestion"
              :index="_key"
            >
              {{ suggestion.item }}
            </slot>
          </template>
          <template
            :slot="`after-section-${cs.name || cs.label}`"
            slot-scope="{section}"
          >
            <slot
              :name="`after-section-${cs.name || cs.label}`"
              :section="section"
            />
          </template>
          <template
            slot="after-section"
            slot-scope="{section}"
          >
            <slot
              name="after-section"
              :section="section"
            />
          </template>
        </component>
        <slot name="after-suggestions" />
      </div>
    </div>
  </div>
</template>

<script>

/**
 * @typedef {Object} ResultSection
 * @prop {String} name - Name of the section
 * @prop {String} label - What is displayed in the section header, is exists
 * @prop {String} type - Used to decide which component to use for section
 * @prop {Number} limit - max number of results
 * @prop {Array} data - the results
 * @prop {Number} start_index - tracks section start reference point
 * @prop {Number} end_index - tracks section end reference point
 */

import DefaultSection from "./parts/DefaultSection.js";
import { addClass, removeClass } from "./utils";

const defaultSectionConfig = {
  name: "default",
  type: "default-section"
}

export default {
  name: "Autosuggest",
  components: {
    /* eslint-disable-next-line vue/no-unused-components */
    DefaultSection
  },
  props: {
    value: {
      type: String,
      default: null
    },
    inputProps: {
      type: Object,
      required: true
    },
    limit: {
      type: Number,
      required: false,
      default: Infinity
    },
    suggestions: {
      type: Array,
      required: true
    },
    renderSuggestion: {
      type: Function,
      required: false,
      default: null
    },
    getSuggestionValue: {
      type: Function,
      required: false,
      default: suggestion => {
        const item = suggestion.item;
        if (typeof item === "object" && item.hasOwnProperty("name")) {
          return item.name;
        } else {
          return item;
        }
      }
    },
    shouldRenderSuggestions: {
      type: Function,
      required: false,
      default: (totalResults, loading) => {
        return totalResults > 0 && !loading;
      }
    },
    sectionConfigs: {
      type: Object,
      required: false,
      default: () => {
        return {
          default: {
            onSelected: null
          }
        };
      }
    },
    onSelected: {
      type: Function,
      required: false,
      default: null
    },
    componentAttrIdAutosuggest: {
      type: String,
      required: false,
      default: "autosuggest"
    },
    componentAttrClassAutosuggestResultsContainer: {
      type: String,
      required: false,
      default: "autosuggest__results-container"
    },
    componentAttrClassAutosuggestResults: {
      type: String,
      required: false,
      default: "autosuggest__results"
    }
  },
  data() {
    return {
      internalValue: null,
      searchInputOriginal: null,
      currentIndex: null,
      currentItem: null,
      loading: false /** Helps with making sure the dropdown doesn't stay open after certain actions */,
      didSelectFromOptions: false,
      internal_inputProps: {}, // Nest default prop values don't work currently in Vue
      defaultInputProps: {
        type: 'text',
        autocomplete: "off",
      },
      clientXMouseDownInitial: null
    };
  },
  computed: {
    listeners() {
      return {
        ...this.$listeners,
        input: e => {
          // Don't do anything native here, since we have inputHandler
          return
        },
        click: () => {
          /* eslint-disable-next-line vue/no-side-effects-in-computed-properties */
          this.loading = false;
          this.$listeners.click && this.$listeners.click(this.currentItem);

          this.ensureItemVisible(this.currentItem, this.currentIndex);
        },
        selected: () => {
          // Determine which onSelected to fire. This can be either from inside
          // a section's object, from the @selected event, or from the deprecated
          // native onSelected prop (to be removed later)
          if (
            this.currentItem &&
            this.sectionConfigs[this.currentItem.name] &&
            this.sectionConfigs[this.currentItem.name].onSelected
          ) {
            this.sectionConfigs[this.currentItem.name].onSelected(
              this.currentItem,
              this.searchInputOriginal
            );
          } else if (this.sectionConfigs["default"].onSelected) {
            this.sectionConfigs["default"].onSelected(null, this.searchInputOriginal);
          } else if (this.$listeners.selected) {
            this.$emit('selected', this.currentItem);
          }
          this.setChangeItem(null)
        }
      };
    },
    isOpen() {
      return this.shouldRenderSuggestions(this.totalResults, this.loading)
    },
    /** @returns {Array<ResultSection>} */
    computedSections() {
      let tmpSize = 0
      return this.suggestions.map(section => {
        if (!section.data) return;

        const name = section.name ? section.name : defaultSectionConfig.name;
        let limit, label, type = null

        if (this.sectionConfigs[name]) {
          limit = this.sectionConfigs[name].limit
          type = this.sectionConfigs[name].type
          label = this.sectionConfigs[name].label
        }

        /** Set defaults for section configs. */
        type = type ? type : defaultSectionConfig.type;

        limit = limit || this.limit
        limit = limit ? limit : Infinity;
        limit = section.data.length < limit ? section.data.length : limit;
        label = label ? label : section.label;

        const computedSection = {
          name,
          label,
          type,
          limit,
          data: section.data,
          start_index: tmpSize,
          end_index: tmpSize + limit - 1
        }

        tmpSize += limit;

        return computedSection
      })
    },
    totalResults () {
      return this.computedSections.reduce((acc, section) => {
        // For each section, make sure we calculate the size
        // based on how many are rendered, which maxes out at
        // the limit but can be less than the limit.
        const { limit, data } = section
        return acc + (data.length >= limit ? limit : data.length)
      }, 0)
    }
  },
  // Watcher to support initialValue
  watch: {
    value: {
      handler(newValue){
        this.internalValue = newValue
      },
      immediate: true
    }
  },
  created() {
    this.internal_inputProps = { ...this.defaultInputProps, ...this.inputProps };
    this.loading = true;
  },
  mounted() {
    document.addEventListener("mouseup", this.onDocumentMouseUp);
    document.addEventListener("mousedown", this.onDocumentMouseDown);
  },
  beforeDestroy() {
    document.removeEventListener("mouseup", this.onDocumentMouseUp)
    document.removeEventListener("mousedown", this.onDocumentMouseDown)
  },
  methods: {
    inputHandler(e) {
      const newValue = e.target.value
      this.$emit('input', newValue)
      this.internalValue = newValue
      if (!this.didSelectFromOptions) {
        this.searchInputOriginal = newValue;
        this.currentIndex = null;
      }
    },
    getSectionRef(i) {
      return "computed_section_" + i;
    },
    getItemByIndex(index) {
      let obj = false;
      if (index === null) return obj;
      for (var i = 0; i < this.computedSections.length; i++) {
        if (
          index >= this.computedSections[i].start_index &&
          index <= this.computedSections[i].end_index
        ) {
          let trueIndex = index - this.computedSections[i].start_index;
          let childSection = this.$refs["computed_section_" + i][0];
          if (childSection) {
            obj = this.normalizeItem(
              this.computedSections[i].name,
              this.computedSections[i].type,
              childSection.section.label,
              childSection.getItemByIndex(trueIndex)
            );
            break;
          }
        }
      }

      return obj;
    },
    handleKeyStroke(e) {
      const { keyCode } = e;

      const ignoredKeyCodes = [
        16, // Shift
        9,  // Tab
        18, // alt/option
        91, // OS Key
        93  // Right OS Key
      ];

      if (ignoredKeyCodes.indexOf(keyCode) > -1) {
        return;
      }

      this.loading = false;
      this.didSelectFromOptions = false;
      switch (keyCode) {
        case 40: // ArrowDown
        case 38: // ArrowUp
          e.preventDefault();
          if (this.isOpen) {
            if (keyCode === 38 && this.currentIndex === null) {
              break;
            }
            // Determine direction of arrow up/down and determine new currentIndex
            const direction = keyCode === 40 ? 1 : -1;
            const newIndex = parseInt(this.currentIndex) + direction;

            this.setCurrentIndex(newIndex, this.totalResults);
            this.didSelectFromOptions = true;
            if (this.totalResults > 0 && this.currentIndex >= 0) {
              this.setChangeItem(this.getItemByIndex(this.currentIndex));
              this.didSelectFromOptions = true;
            } else if (this.currentIndex == -1) {
              this.currentIndex = null;
              this.internalValue = this.searchInputOriginal;
              e.preventDefault();
            }
          }
          break;
        case 13: // Enter
          e.preventDefault();

          if (this.totalResults > 0 && this.currentIndex >= 0) {
            this.setChangeItem(this.getItemByIndex(this.currentIndex), true);
            this.didSelectFromOptions = true;
          }

          this.loading = true;
          this.listeners.selected(this.didSelectFromOptions);
          break;
        case 27: // Escape
          if (this.isOpen) {
            /* For 'search' input type, make sure the browser doesn't clear the input when Escape is pressed. */
            this.loading = true;
            this.currentIndex = null;
            this.internalValue = this.searchInputOriginal
            this.$emit('input', this.searchInputOriginal);
            e.preventDefault();
          }
          break;
      }
    },
    setChangeItem(item, overrideOriginalInput = false) {
      if (this.currentIndex === null || !item) {
        this.currentItem = null;
      } else if (item) {
        this.currentItem = item;
        const v = this.getSuggestionValue(item)
        this.internalValue = v;
        if (overrideOriginalInput) {
          this.searchInputOriginal = v;
        }
        this.ensureItemVisible(item, this.currentIndex);
      }
    },
    normalizeItem(name, type, label, item) {
      return {
        name,
        type,
        label,
        item
      };
    },
    ensureItemVisible(item, index) {
      const resultsScrollElement = this.$el.querySelector(
        `.${this.componentAttrClassAutosuggestResults}`
      );

      if (!item || (!index && index !== 0) || !resultsScrollElement) {
        return;
      }

      const itemElement = this.$el.querySelector(`#autosuggest__results-item--${index}`);
      if (!itemElement) {
        return;
      }

      const resultsScrollWindowHeight = resultsScrollElement.clientHeight;
      const resultsScrollScrollTop = resultsScrollElement.scrollTop;

      const itemHeight = itemElement.clientHeight;
      const currentItemScrollOffset = itemElement.offsetTop;

      if (
        itemHeight + currentItemScrollOffset >=
        resultsScrollScrollTop + resultsScrollWindowHeight
      ) {
        /** Current item goes below visible scroll window */
        resultsScrollElement.scrollTop = itemHeight + currentItemScrollOffset - resultsScrollWindowHeight;
      } else if (currentItemScrollOffset < resultsScrollScrollTop && resultsScrollScrollTop > 0) {
        /** Current item goes above visible scroll window */
        resultsScrollElement.scrollTop = currentItemScrollOffset;
      }
    },
    updateCurrentIndex(index) {
      this.setCurrentIndex(index, -1, true);
    },
    clickedOnScrollbar(e, mouseX){
      const results = this.$el.querySelector(`.${this.componentAttrClassAutosuggestResults}`);

      const mouseIsInsideScrollbar = results && results.clientWidth <= (mouseX + 17) &&
        mouseX + 17 <= results.clientWidth + 34
      return e.target.tagName === 'DIV' && results && mouseIsInsideScrollbar || false;
    },
    onDocumentMouseDown(e) {
      var rect = e.target.getBoundingClientRect ? e.target.getBoundingClientRect() : 0;
      this.clientXMouseDownInitial = e.clientX - rect.left;
    },
    onDocumentMouseUp(e) {
      /** Do not re-render list on input click  */
      const isChild = this.$el.contains(e.target);

      if (isChild && e.target.tagName === 'INPUT' ||
        (this.clickedOnScrollbar(e, this.clientXMouseDownInitial))) {
        return;
      }

      /** Clicks outside of dropdown to exit */
      if (this.currentIndex === null || !this.isOpen) {
        this.loading = true;
        return;
      }

      /** Selects an item in the dropdown */
      this.loading = true;
      this.didSelectFromOptions = true;
      this.setChangeItem(this.getItemByIndex(this.currentIndex), true);
      this.listeners.selected(true);
    },

    setCurrentIndex(newIndex, limit = -1, onHover = false) {
      let adjustedValue = newIndex;

      if (!onHover){
        const hitLowerLimt = this.currentIndex === null
        const hitUpperLimit = newIndex >= limit
        if (hitLowerLimt || hitUpperLimit) {
          adjustedValue = 0;
        }
      }

      this.currentIndex = adjustedValue;
      const element = this.$el.querySelector(`#autosuggest__results-item--${this.currentIndex}`);
      const hoverClass = "autosuggest__results-item--highlighted";

      if (this.$el.querySelector(`.${hoverClass}`)) {
        removeClass(this.$el.querySelector(`.${hoverClass}`), hoverClass);
      }
      if (element) {
        addClass(element, hoverClass);
      }
    }
  },
};
</script>

<template>
  <div :id="componentAttrIdAutosuggest">
    <slot name="before-input" /><input
      :type="inputProps['type'] ? inputProps['type'] : 'text'"
      :value="internalValue"
      :autocomplete="inputProps.autocomplete"
      role="combobox"
      :class="[isOpen ? 'autosuggest__input--open' : '', inputProps['class']]"
      v-bind="inputProps"
      aria-autocomplete="list"
      aria-owns="autosuggest__results"
      :aria-activedescendant="isOpen && currentIndex !== null ? `autosuggest__results-item--${currentIndex}` : ''"
      :aria-haspopup="isOpen ? 'true' : 'false'"
      :aria-expanded="isOpen ? 'true' : 'false'"
      @input="inputHandler"
      @keydown="handleKeyStroke"
      v-on="listeners"
    ><slot name="after-input" />
    <div :class="componentAttrClassAutosuggestResultsContainer">
      <div
        v-if="isOpen"
        :class="componentAttrClassAutosuggestResults"
        :aria-labelledby="componentAttrIdAutosuggest"
      >
        <slot name="before-suggestions" />
        <component
          :is="cs.type"
          v-for="(cs, key) in computedSections"
          :ref="getSectionRef(key)"
          :key="getSectionRef(key)"
          :current-index="currentIndex"
          :normalize-item-function="normalizeItem"
          :render-suggestion="renderSuggestion"
          :section="cs"
          @updateCurrentIndex="updateCurrentIndex"
        >
          <template
            :slot="`before-section-${cs.name || cs.label}`"
            slot-scope="{section, className}"
          >
            <slot
              :name="`before-section-${cs.name || cs.label}`"
              :section="section"
              :className="className"
            />
          </template>
          <template slot-scope="{ suggestion, _key }">
            <slot
              :suggestion="suggestion"
              :index="_key"
            >
              {{ suggestion.item }}
            </slot>
          </template>
          <template
            :slot="`after-section-${cs.name || cs.label}`"
            slot-scope="{section}"
          >
            <slot
              :name="`after-section-${cs.name || cs.label}`"
              :section="section"
            />
          </template>
          <template
            slot="after-section"
            slot-scope="{section}"
          >
            <slot
              name="after-section"
              :section="section"
            />
          </template>
        </component>
        <slot name="after-suggestions" />
      </div>
    </div>
  </div>
</template>

<script>

/**
 * @typedef {Object} ResultSection
 * @prop {String} name - Name of the section
 * @prop {String} label - What is displayed in the section header, is exists
 * @prop {String} type - Used to decide which component to use for section
 * @prop {Number} limit - max number of results
 * @prop {Array} data - the results
 * @prop {Number} start_index - tracks section start reference point
 * @prop {Number} end_index - tracks section end reference point
 */

import DefaultSection from "./parts/DefaultSection.js";
import { addClass, removeClass } from "./utils";

const defaultSectionConfig = {
  name: "default",
  type: "default-section"
}

export default {
  name: "Autosuggest",
  components: {
    /* eslint-disable-next-line vue/no-unused-components */
    DefaultSection
  },
  props: {
    value: {
      type: String,
      default: null
    },
    inputProps: {
      type: Object,
      required: true
    },
    limit: {
      type: Number,
      required: false,
      default: Infinity
    },
    suggestions: {
      type: Array,
      required: true
    },
    renderSuggestion: {
      type: Function,
      required: false,
      default: null
    },
    getSuggestionValue: {
      type: Function,
      required: false,
      default: suggestion => {
        const item = suggestion.item;
        if (typeof item === "object" && item.hasOwnProperty("name")) {
          return item.name;
        } else {
          return item;
        }
      }
    },
    shouldRenderSuggestions: {
      type: Function,
      required: false,
      default: (totalResults, loading) => {
        return totalResults > 0 && !loading;
      }
    },
    sectionConfigs: {
      type: Object,
      required: false,
      default: () => {
        return {
          default: {
            onSelected: null
          }
        };
      }
    },
    onSelected: {
      type: Function,
      required: false,
      default: null
    },
    componentAttrIdAutosuggest: {
      type: String,
      required: false,
      default: "autosuggest"
    },
    componentAttrClassAutosuggestResultsContainer: {
      type: String,
      required: false,
      default: "autosuggest__results-container"
    },
    componentAttrClassAutosuggestResults: {
      type: String,
      required: false,
      default: "autosuggest__results"
    }
  },
  data() {
    return {
      internalValue: null,
      searchInputOriginal: null,
      currentIndex: null,
      currentItem: null,
      loading: false /** Helps with making sure the dropdown doesn't stay open after certain actions */,
      didSelectFromOptions: false,
      internal_inputProps: {}, // Nest default prop values don't work currently in Vue
      defaultInputProps: {
        autocomplete: "off",
      },
      clientXMouseDownInitial: null
    };
  },
  computed: {
    listeners() {
      return {
        ...this.$listeners,
        input: e => {
          // Don't do anything native here, since we have inputHandler
          return
        },
        click: () => {
          /* eslint-disable-next-line vue/no-side-effects-in-computed-properties */
          this.loading = false;
          this.$listeners.click && this.$listeners.click(this.currentItem);

          this.ensureItemVisible(this.currentItem, this.currentIndex);
        },
        selected: () => {
          // Determine which onSelected to fire. This can be either from inside
          // a section's object, from the @selected event, or from the deprecated
          // native onSelected prop (to be removed later)
          if (
            this.currentItem &&
            this.sectionConfigs[this.currentItem.name] &&
            this.sectionConfigs[this.currentItem.name].onSelected
          ) {
            this.sectionConfigs[this.currentItem.name].onSelected(
              this.currentItem,
              this.searchInputOriginal
            );
          } else if (this.sectionConfigs["default"].onSelected) {
            this.sectionConfigs["default"].onSelected(null, this.searchInputOriginal);
          } else if (this.$listeners.selected) {
            this.$emit('selected', this.currentItem);
          }
          this.setChangeItem(null)
        }
      };
    },
    isOpen() {
      return this.shouldRenderSuggestions(this.totalResults, this.loading)
    },
    /** @returns {Array<ResultSection>} */
    computedSections() {
      let tmpSize = 0
      return this.suggestions.map(section => {
        if (!section.data) return;

        const name = section.name ? section.name : defaultSectionConfig.name;
        let limit, label, type = null

        if (this.sectionConfigs[name]) {
          limit = this.sectionConfigs[name].limit
          type = this.sectionConfigs[name].type
          label = this.sectionConfigs[name].label
        }

        /** Set defaults for section configs. */
        type = type ? type : defaultSectionConfig.type;

        limit = limit || this.limit
        limit = limit ? limit : Infinity;
        limit = section.data.length < limit ? section.data.length : limit;
        label = label ? label : section.label;

        const computedSection = {
          name,
          label,
          type,
          limit,
          data: section.data,
          start_index: tmpSize,
          end_index: tmpSize + limit - 1
        }

        tmpSize += limit;

        return computedSection
      })
    },
    totalResults () {
      return this.computedSections.reduce((acc, section) => {
        // For each section, make sure we calculate the size
        // based on how many are rendered, which maxes out at
        // the limit but can be less than the limit.
        const { limit, data } = section
        return acc + (data.length >= limit ? limit : data.length)
      }, 0)
    }
  },
  // Watcher to support initialValue
  watch: {
    value: {
      handler(newValue){
        this.internalValue = newValue
      },
      immediate: true
    }
  },
  created() {
    /** Take care of nested input props */
    this.internal_inputProps = { ...this.defaultInputProps, ...this.inputProps };
    this.inputProps.autocomplete = this.internal_inputProps.autocomplete;
    this.loading = true;
  },
  mounted() {
    document.addEventListener("mouseup", this.onDocumentMouseUp);
    document.addEventListener("mousedown", this.onDocumentMouseDown);
  },
  beforeDestroy() {
    document.removeEventListener("mouseup", this.onDocumentMouseUp)
    document.removeEventListener("mousedown", this.onDocumentMouseDown)
  },
  methods: {
    inputHandler(e) {
      const newValue = e.target.value
      this.$emit('input', newValue)
      this.internalValue = newValue
      if (!this.didSelectFromOptions) {
        this.searchInputOriginal = newValue;
        this.currentIndex = null;
      }
    },
    getSectionRef(i) {
      return "computed_section_" + i;
    },
    getItemByIndex(index) {
      let obj = false;
      if (index === null) return obj;
      for (var i = 0; i < this.computedSections.length; i++) {
        if (
          index >= this.computedSections[i].start_index &&
          index <= this.computedSections[i].end_index
        ) {
          let trueIndex = index - this.computedSections[i].start_index;
          let childSection = this.$refs["computed_section_" + i][0];
          if (childSection) {
            obj = this.normalizeItem(
              this.computedSections[i].name,
              this.computedSections[i].type,
              childSection.section.label,
              childSection.getItemByIndex(trueIndex)
            );
            break;
          }
        }
      }

      return obj;
    },
    handleKeyStroke(e) {
      const { keyCode } = e;

      const ignoredKeyCodes = [
        16, // Shift
        9,  // Tab
        18, // alt/option
        91, // OS Key
        93  // Right OS Key
      ];

      if (ignoredKeyCodes.indexOf(keyCode) > -1) {
        return;
      }

      this.loading = false;
      this.didSelectFromOptions = false;
      switch (keyCode) {
        case 40: // ArrowDown
        case 38: // ArrowUp
          e.preventDefault();
          if (this.isOpen) {
            if (keyCode === 38 && this.currentIndex === null) {
              break;
            }
            // Determine direction of arrow up/down and determine new currentIndex
            const direction = keyCode === 40 ? 1 : -1;
            const newIndex = parseInt(this.currentIndex) + direction;

            this.setCurrentIndex(newIndex, this.totalResults);
            this.didSelectFromOptions = true;
            if (this.totalResults > 0 && this.currentIndex >= 0) {
              this.setChangeItem(this.getItemByIndex(this.currentIndex));
              this.didSelectFromOptions = true;
            } else if (this.currentIndex == -1) {
              this.currentIndex = null;
              this.internalValue = this.searchInputOriginal;
              e.preventDefault();
            }
          }
          break;
        case 13: // Enter
          e.preventDefault();

          if (this.totalResults > 0 && this.currentIndex >= 0) {
            this.setChangeItem(this.getItemByIndex(this.currentIndex), true);
            this.didSelectFromOptions = true;
          }

          this.loading = true;
          this.listeners.selected(this.didSelectFromOptions);
          break;
        case 27: // Escape
          if (this.isOpen) {
            /* For 'search' input type, make sure the browser doesn't clear the input when Escape is pressed. */
            this.loading = true;
            this.currentIndex = null;
            this.internalValue = this.searchInputOriginal
            this.$emit('input', this.searchInputOriginal);
            e.preventDefault();
          }
          break;
      }
    },
    setChangeItem(item, overrideOriginalInput = false) {
      if (this.currentIndex === null || !item) {
        this.currentItem = null;
      } else if (item) {
        this.currentItem = item;
        const v = this.getSuggestionValue(item)
        this.internalValue = v;
        if (overrideOriginalInput) {
          this.searchInputOriginal = v;
        }
        this.ensureItemVisible(item, this.currentIndex);
      }
    },
    normalizeItem(name, type, label, item) {
      return {
        name,
        type,
        label,
        item
      };
    },
    ensureItemVisible(item, index) {
      const resultsScrollElement = this.$el.querySelector(
        `.${this.componentAttrClassAutosuggestResults}`
      );

      if (!item || (!index && index !== 0) || !resultsScrollElement) {
        return;
      }

      const itemElement = this.$el.querySelector(`#autosuggest__results-item--${index}`);
      if (!itemElement) {
        return;
      }

      const resultsScrollWindowHeight = resultsScrollElement.clientHeight;
      const resultsScrollScrollTop = resultsScrollElement.scrollTop;

      const itemHeight = itemElement.clientHeight;
      const currentItemScrollOffset = itemElement.offsetTop;

      if (
        itemHeight + currentItemScrollOffset >=
        resultsScrollScrollTop + resultsScrollWindowHeight
      ) {
        /** Current item goes below visible scroll window */
        resultsScrollElement.scrollTop = itemHeight + currentItemScrollOffset - resultsScrollWindowHeight;
      } else if (currentItemScrollOffset < resultsScrollScrollTop && resultsScrollScrollTop > 0) {
        /** Current item goes above visible scroll window */
        resultsScrollElement.scrollTop = currentItemScrollOffset;
      }
    },
    updateCurrentIndex(index) {
      this.setCurrentIndex(index, -1, true);
    },
    clickedOnScrollbar(e, mouseX){
      const results = this.$el.querySelector(`.${this.componentAttrClassAutosuggestResults}`);

      const mouseIsInsideScrollbar = results && results.clientWidth <= (mouseX + 17) &&
        mouseX + 17 <= results.clientWidth + 34
      return e.target.tagName === 'DIV' && results && mouseIsInsideScrollbar || false;
    },
    onDocumentMouseDown(e) {
      var rect = e.target.getBoundingClientRect ? e.target.getBoundingClientRect() : 0;
      this.clientXMouseDownInitial = e.clientX - rect.left;
    },
    onDocumentMouseUp(e) {
      /** Do not re-render list on input click  */
      const isChild = this.$el.contains(e.target);

      if (isChild && e.target.tagName === 'INPUT' ||
        (this.clickedOnScrollbar(e, this.clientXMouseDownInitial))) {
        return;
      }

      /** Clicks outside of dropdown to exit */
      if (this.currentIndex === null || !this.isOpen) {
        this.loading = true;
        return;
      }

      /** Selects an item in the dropdown */
      this.loading = true;
      this.didSelectFromOptions = true;
      this.setChangeItem(this.getItemByIndex(this.currentIndex), true);
      this.listeners.selected(true);
    },

    setCurrentIndex(newIndex, limit = -1, onHover = false) {
      let adjustedValue = newIndex;

      if (!onHover){
        const hitLowerLimt = this.currentIndex === null
        const hitUpperLimit = newIndex >= limit
        if (hitLowerLimt || hitUpperLimit) {
          adjustedValue = 0;
        }
      }

      this.currentIndex = adjustedValue;
      const element = this.$el.querySelector(`#autosuggest__results-item--${this.currentIndex}`);
      const hoverClass = "autosuggest__results-item--highlighted";

      if (this.$el.querySelector(`.${hoverClass}`)) {
        removeClass(this.$el.querySelector(`.${hoverClass}`), hoverClass);
      }
      if (element) {
        addClass(element, hoverClass);
      }
    }
  },
};
</script>