0

I am implementing a customized dropdown becuase of the requirements we have, using Vue 2 and typescript (jquery is not an option).

It is working fine, when you click on the main box, it opens the options list downwards.

An improvement I am looking for is that, when at the end of the screen, the options list adds to the page height and thus causing the scrollbar to appear or increase scroll height.

What I am looking for is that, when popping up the div, if there's not enough space at the bottom of the screen, open it upwards instead. How do I achieve this? (classes are using bootstrat 5)

Opened dropdown & Closed dropdown

My code:

import Vue, {
  PropType
} from 'vue';
import {
  Validation
} from 'vuelidate';

let uidc = 0;

export default Vue.extend({
  name: 'BaseDropdown',
  props: {
    value: {
      type: [Number, String, Object],
      default: () => ''
      as string,
    },
    target: {
      type: String,
      default: '',
    },
    label: {
      type: String,
      default: '',
    },
    valueIsNumber: {
      type: Boolean,
      default: false,
    },
    options: {
      type: Array,
      default: null,
    },
    placeholder: {
      type: String,
      default: '',
    },
    required: {
      type: Boolean,
      default: false,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    validations: {
      type: Object as PropType < Validation > ,
      default: () => ({
        $error: false,
        $touch: () => undefined,
        $params: {},
      }) as Validation,
    },
    error: {
      type: Boolean,
      default: false,
    },
    trackEvent: {
      type: String,
      default: '',
    },
    trackField: {
      type: String,
      default: '',
    },
    trackPublic: {
      type: Boolean,
      default: false,
    },
    padLeft: {
      type: Boolean,
      default: false,
    },
    enforceBlackColour: {
      type: Boolean,
      default: false,
    },
    customStyled: {
      type: Boolean,
      default: false,
    },
    borderBottomWarning: {
      type: Boolean,
      default: false,
    },
  },
  data(): {
    selectedItem: any | null;
    menuOpen: boolean;
    searchText: string | null;
  } {
    return {
      selectedItem: null,
      menuOpen: false,
      searchText: null,
    };
  },
  mounted() {
    const appElement = document.getElementById('app_home');
    (appElement as any).addEventListener('click', this.handleDropdownClickOutside);
    this.$nextTick(() => {
      if (this.value) {
        if (this.valueIsNumber) {
          this.selectedItem = this.options.find((x: any) => x.value === Number(this.value)) || null;
        } else {
          this.selectedItem = this.options.find((x: any) => x.value.toString().toLowerCase() === this.value.toString().toLowerCase()) || null;
        }
      }
    });
  },
  computed: {
    v(): Validation | {} {
      return this.validations;
    },
    errorMessage(): string {
      // Validation must be cast to any to access validators
      return Object.entries((this.v as Validation).$params).find(([k]) => !(this.v as any)[k]) ? .[1].message;
    },
    optgroups(): any {
      return this.options.reduce((acc: any, o: any) => ({ ...acc,
        [o.optgroup]: [...(acc[o.optgroup] || []), o]
      }), {});
    },
    isRequired(): boolean {
      return this.required !== false;
    },
    getSelectedItemText(): string | null {
      return this.selectedItem ? this.selectedItem.text : this.placeholder || 'Please select an item';
    },
    filteredItems(): any[] {
      const list: any[] = [];
      for (let c = 0; c < 10; c += 1) {
        list.push({
          text: c,
          value: c
        });
      }
      // return this.searchText && this.searchText.length > 0 ? this.options.filter((x: any) => x.text.toLowerCase().indexOf(this.searchText!.toLowerCase()) > -1) : this.options;
      return list;
    },
  },
  methods: {
    openMenu() {
      this.menuOpen = !this.menuOpen;
      if (this.menuOpen) {
        this.searchText = null;
      }
    },
    selectItem(item: any) {
      this.selectedItem = item;
      this.$emit('input', item.value);
      this.menuOpen = false;
    },
    setSuppliedSelectedItem() {
      this.$nextTick(() => {
        if (this.value) {
          this.selectedItem = this.options.find((x: any) => x.value === this.value) || null;
        }
      });
    },
    handleDropdownClickOutside(event: any): void {
      const parent = document.getElementById(`select-${(this as any).uid}`);
      const isParent = parent !== event.target && parent ? .contains(event.target);
      if (!isParent) {
        this.menuOpen = false;
        // this.closeOpenendMenu();
        // this.searchText = '';
      }
    },
  },
  beforeCreate() {
    // eslint-disable-next-line no-plusplus
    (this as any).uid = uidc++;
  },
});
.dropdown {
  font-size: 0.7rem;
  img {
    // float: right;
    // padding-right: 10px;
    // padding-top: 5px;
    position: absolute;
    top: 40%;
    right: 10px;
  }
  .fade {
    opacity: 0.5;
  }
  .search-box {
    .form-control {
      font-size: 12px !important;
      height: 30px !important;
      margin: 0 10px 5px 10px !important;
      width: 95% !important;
    }
  }
  .selected-item {
    border-radius: 3px;
    border: 1px solid #ced4da;
    padding: 10px;
    .selected-item-text {
      text-overflow: ellipsis;
      overflow: hidden;
      width: 93%;
      /* height: 1.2em; */
      white-space: nowrap;
    }
  }
  .items {
    border: 1px solid rgb(236, 236, 236);
    width: 100%;
    z-index: 15;
    max-height: 300px;
    overflow-y: auto;
    overflow-x: hidden;
    background-color: white;
  }
  .item {
    padding: 10px;
    background-color: rgb(240, 240, 240);
    cursor: pointer;
    &:hover {
      background-color: rgb(216, 216, 216);
    }
  }
}

.hidden {
  opacity: 0.2;
}

.disabled {
  background-color: #e9ecef;
  opacity: 1;
  pointer-events: none;
}
<template>
<div class="mt-2" :id="`select-${uid}`">
  <label v-show="label" class="mb-2 label-grey" :class="{ 'required': isRequired }" :for="`select-${uid}`">{{ label }}</label>
  <div class="dropdown noselect position-relative" :class="{'disabled': disabled}">
    <div class="selected-item cursor-pointer" @click="openMenu">
      <div class="selected-item-text" :class="{'fade': !selectedItem}">{{getSelectedItemText}}</div>
      <img v-if="menuOpen" :src="constants.icons.arrowTop" />
      <img v-else :src="constants.icons.arrowDown" />
    </div>
    <div class="items position-absolute" v-show="menuOpen">
      <div v-if="filteredItems && filteredItems.length > 5 || searchText" class="search-box">
        <input :size="'sm'" v-model="searchText" />
      </div>
      <div v-for="item in filteredItems" :key="item.value" @click="selectItem(item)">
        <div class="item">
          {{item.text}}
        </div>
      </div>
    </div>
  </div>
  <span v-if="v.$error" class="text-error text-xs font-light">{{ errorMessage }}</span>
</div>
</template>
ℛɑƒæĿᴿᴹᴿ
  • 4,983
  • 4
  • 38
  • 58
Ali Makhmali
  • 1
  • 1
  • 1

1 Answers1

0

Suggest to use Floating-ui (well known as Poper)

Floating UI is a low-level library for positioning "floating" elements...intelligently keeping them in view

It's been using widely and cover a lot of edge cases you might encounter when try to create dropdown yourself

You can try with references here

nart
  • 1,508
  • 2
  • 11
  • 24