mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
enhance select dropdown + combobox
This commit is contained in:
parent
197436c9d5
commit
c18c23e785
7 changed files with 85 additions and 121 deletions
|
|
@ -177,11 +177,11 @@ body {
|
|||
}
|
||||
|
||||
.input-header-label {
|
||||
@apply relative lowercase capitalize-first mb-1 transition duration-300 ease-in-out text-sm font-bold m-0 dark:text-gray-300 mr-2;
|
||||
@apply relative lowercase capitalize-first mb-1 transition duration-300 ease-in-out text-sm font-bold m-0 dark:text-gray-300 mr-1;
|
||||
}
|
||||
|
||||
.input-header-required-sign {
|
||||
@apply font-bold text-red-500 absolute ml-1;
|
||||
@apply font-bold text-red-500 absolute;
|
||||
}
|
||||
|
||||
.input-error-msg {
|
||||
|
|
@ -229,8 +229,12 @@ body {
|
|||
@apply pointer-events-none transition-transform h-4 w-4 fill-gray-600 dark:fill-gray-500;
|
||||
}
|
||||
|
||||
.select-combobox-dropdown-container {
|
||||
@apply flex w-fit mt-2;
|
||||
}
|
||||
|
||||
.select-combobox-list {
|
||||
@apply flex flex-col;
|
||||
@apply flex flex-col max-h-[200px] overflow-x-hidden overflow-y-auto;
|
||||
}
|
||||
|
||||
.combobox-no-match {
|
||||
|
|
@ -238,15 +242,15 @@ body {
|
|||
}
|
||||
|
||||
.select-dropdown-container {
|
||||
@apply flex fixed h-full max-h-[200px] overflow-x-hidden overflow-y-auto flex-col w-fit mt-2;
|
||||
@apply flex max-h-[200px] overflow-x-hidden overflow-y-auto flex-col w-fit mt-2;
|
||||
}
|
||||
|
||||
.open.select-dropdown-container {
|
||||
@apply z-[20] opacity-100;
|
||||
@apply absolute z-[20] opacity-100;
|
||||
}
|
||||
|
||||
.close.select-dropdown-container {
|
||||
@apply -z-10 opacity-0 pointer-events-none;
|
||||
@apply -z-10 fixed opacity-0 pointer-events-none;
|
||||
}
|
||||
|
||||
.select-dropdown-btn {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -135,7 +135,7 @@ watch(props, (newProp, oldProp) => {
|
|||
});
|
||||
|
||||
const inp = reactive({
|
||||
value: props.value,
|
||||
value: "",
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
|
|
@ -166,66 +166,37 @@ function toggleSelect() {
|
|||
select.isOpen = select.isOpen ? false : true;
|
||||
// Position dropdown relative to btn on open on fixed position
|
||||
if (select.isOpen) {
|
||||
// focus on combobox and reset value
|
||||
// Reset input value
|
||||
inp.value = "";
|
||||
inp.isValid = inputEl.value.checkValidity();
|
||||
inputEl.value.focus();
|
||||
|
||||
// setup dropdown position
|
||||
// Get field container rect
|
||||
const fieldContainer = selectBtn.value.closest("[data-field-container]");
|
||||
const parent = fieldContainer.parentElement;
|
||||
// Update position only if parent has overflow
|
||||
const isOverflow = parent.scrollHeight > parent.clientHeight ? true : false;
|
||||
if (!isOverflow) return;
|
||||
|
||||
// Get all rect
|
||||
const selectBtnRect = selectBtn.value.getBoundingClientRect();
|
||||
const selectDropdownRect = selectDropdown.value.getBoundingClientRect();
|
||||
const fieldContainerRect = fieldContainer.getBoundingClientRect();
|
||||
const selectDropRect = selectDropdown.value.getBoundingClientRect();
|
||||
|
||||
// We need to take care of parent padding and margin that will affect dropdown position but aren't calculate in rect
|
||||
const parents = [selectBtn.value.parentElement];
|
||||
let isParent = selectBtn.value.parentElement ? true : false;
|
||||
while (isParent) {
|
||||
parents.push(parents[parents.length - 1].parentElement);
|
||||
isParent = parents[parents.length - 1].parentElement ? true : false;
|
||||
}
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
||||
let noRectParentHeight = 0;
|
||||
for (let i = 0; i < parents.length; i++) {
|
||||
try {
|
||||
noRectParentHeight += +window
|
||||
.getComputedStyle(parents[i], null)
|
||||
.getPropertyValue("padding-top")
|
||||
.replace("px", "");
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
noRectParentHeight += +window
|
||||
.getComputedStyle(parents[i], null)
|
||||
.getPropertyValue("margin-top")
|
||||
.replace("px", "");
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// If dropdown is too close to bottom, we need to drop it up
|
||||
const canDropBeDown =
|
||||
selectBtnRect.top + selectDropdownRect.height + 20 < window.innerHeight
|
||||
const canBeDown =
|
||||
fieldContainerRect.bottom + selectDropRect.height < parentRect.bottom
|
||||
? true
|
||||
: false;
|
||||
|
||||
if (canDropBeDown) {
|
||||
selectDropdown.value.style.top = `${
|
||||
window.scrollY +
|
||||
selectBtnRect.bottom +
|
||||
selectBtnRect.height * 2 -
|
||||
selectDropdownRect.height -
|
||||
noRectParentHeight +
|
||||
16
|
||||
if (!canBeDown) {
|
||||
selectDropdown.value.style.top = `-${
|
||||
selectDropRect.height + selectBtnRect.height
|
||||
}px`;
|
||||
}
|
||||
|
||||
if (!canDropBeDown) {
|
||||
selectDropdown.value.style.top = `${
|
||||
window.scrollY +
|
||||
selectBtnRect.top +
|
||||
selectBtnRect.height * 2 -
|
||||
selectDropdownRect.height * 2 -
|
||||
noRectParentHeight -
|
||||
24
|
||||
}px`;
|
||||
if (canBeDown) {
|
||||
selectDropdown.value.style.top = `${selectBtnRect.height}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -295,6 +266,8 @@ const emits = defineEmits(["inp"]);
|
|||
|
||||
<template>
|
||||
<Container
|
||||
data-field-container
|
||||
:class="[select.isOpen ? 'z-[100]' : '']"
|
||||
:containerClass="`field-container ${props.containerClass}`"
|
||||
:columns="props.columns"
|
||||
>
|
||||
|
|
@ -334,7 +307,6 @@ const emits = defineEmits(["inp"]);
|
|||
:aria-controls="`${props.id}-custom`"
|
||||
:aria-expanded="select.isOpen ? 'true' : 'false'"
|
||||
:aria-description="$t('inp_select_dropdown_button_desc')"
|
||||
data-select-dropdown
|
||||
:disabled="props.disabled || false"
|
||||
@click.prevent="toggleSelect()"
|
||||
:class="[
|
||||
|
|
@ -380,6 +352,7 @@ const emits = defineEmits(["inp"]);
|
|||
:tabindex="select.isOpen ? props.tabId : '-1'"
|
||||
ref="inputEl"
|
||||
v-model="inp.value"
|
||||
:placeholder="$t('inp_combobox_placeholder')"
|
||||
@input="
|
||||
() => {
|
||||
inp.isValid = inputEl.checkValidity();
|
||||
|
|
@ -393,7 +366,6 @@ const emits = defineEmits(["inp"]);
|
|||
inp.isValid ? 'valid' : 'invalid',
|
||||
props.inpClass,
|
||||
]"
|
||||
:placeholder="props.placeholder || ''"
|
||||
:pattern="props.pattern || '(?s).*'"
|
||||
:name="`${props.id}-combobox`"
|
||||
:value="inp.value"
|
||||
|
|
@ -415,6 +387,7 @@ const emits = defineEmits(["inp"]);
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-select-dropdown
|
||||
:id="`${props.id}-list`"
|
||||
:aria-hidden="select.isOpen ? 'false' : 'true'"
|
||||
role="radiogroup"
|
||||
|
|
@ -434,8 +407,6 @@ const emits = defineEmits(["inp"]);
|
|||
role="radio"
|
||||
@click.prevent="$emit('inp', changeValue(value))"
|
||||
:class="[
|
||||
id === 0 ? 'first' : '',
|
||||
id === props.values.length - 1 ? 'last' : '',
|
||||
(value === select.value && select.value === value) ||
|
||||
(!select.value && value === props.value)
|
||||
? 'active'
|
||||
|
|
|
|||
|
|
@ -636,6 +636,7 @@ function setIndex(calendarEl, tabindex) {
|
|||
|
||||
<template>
|
||||
<Container
|
||||
:class="[picker.isOpen ? 'z-[100]' : '']"
|
||||
v-if="props.inpType === 'datepicker'"
|
||||
:containerClass="`field-container ${props.containerClass}`"
|
||||
:columns="props.columns"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { v4 as uuidv4 } from "uuid";
|
|||
iconColor: "info",
|
||||
},]
|
||||
}
|
||||
@param {string} [id=uuidv4()] - Unique id
|
||||
@param {string} [id=uuidv4()] - Unique id
|
||||
@param {string} label - The label of the field. Can be a translation key or by default raw text.
|
||||
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
|
||||
@param {string} value
|
||||
|
|
@ -158,62 +158,35 @@ const selectDropdown = ref();
|
|||
// EVENTS
|
||||
function toggleSelect() {
|
||||
select.isOpen = select.isOpen ? false : true;
|
||||
// Position dropdown relative to btn on open on fixed position
|
||||
// Check if parent has overflow
|
||||
if (select.isOpen) {
|
||||
// Get field container rect
|
||||
const fieldContainer = selectBtn.value.closest("[data-field-container]");
|
||||
const parent = fieldContainer.parentElement;
|
||||
// Update position only if parent has overflow
|
||||
const isOverflow = parent.scrollHeight > parent.clientHeight ? true : false;
|
||||
if (!isOverflow) return;
|
||||
|
||||
// Get all rect
|
||||
const selectBtnRect = selectBtn.value.getBoundingClientRect();
|
||||
const selectDropdownRect = selectDropdown.value.getBoundingClientRect();
|
||||
const fieldContainerRect = fieldContainer.getBoundingClientRect();
|
||||
const selectDropRect = selectDropdown.value.getBoundingClientRect();
|
||||
|
||||
// We need to take care of parent padding and margin that will affect dropdown position but aren't calculate in rect
|
||||
const parents = [selectBtn.value.parentElement];
|
||||
let isParent = selectBtn.value.parentElement ? true : false;
|
||||
while (isParent) {
|
||||
parents.push(parents[parents.length - 1].parentElement);
|
||||
isParent = parents[parents.length - 1].parentElement ? true : false;
|
||||
}
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
||||
let noRectParentHeight = 0;
|
||||
for (let i = 0; i < parents.length; i++) {
|
||||
try {
|
||||
noRectParentHeight += +window
|
||||
.getComputedStyle(parents[i], null)
|
||||
.getPropertyValue("padding-top")
|
||||
.replace("px", "");
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
noRectParentHeight += +window
|
||||
.getComputedStyle(parents[i], null)
|
||||
.getPropertyValue("margin-top")
|
||||
.replace("px", "");
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// If dropdown is too close to bottom, we need to drop it up
|
||||
const canDropBeDown =
|
||||
selectBtnRect.top + selectDropdownRect.height + 20 < window.innerHeight
|
||||
const canBeDown =
|
||||
fieldContainerRect.bottom + selectDropRect.height < parentRect.bottom
|
||||
? true
|
||||
: false;
|
||||
|
||||
if (canDropBeDown) {
|
||||
selectDropdown.value.style.top = `${
|
||||
window.scrollY +
|
||||
selectBtnRect.bottom +
|
||||
selectBtnRect.height * 2 -
|
||||
selectDropdownRect.height -
|
||||
noRectParentHeight +
|
||||
16
|
||||
if (!canBeDown) {
|
||||
selectDropdown.value.style.top = `-${
|
||||
selectDropRect.height + selectBtnRect.height
|
||||
}px`;
|
||||
}
|
||||
|
||||
if (!canDropBeDown) {
|
||||
selectDropdown.value.style.top = `${
|
||||
window.scrollY +
|
||||
selectBtnRect.top +
|
||||
selectBtnRect.height * 2 -
|
||||
selectDropdownRect.height * 2 -
|
||||
noRectParentHeight -
|
||||
24
|
||||
}px`;
|
||||
if (canBeDown) {
|
||||
selectDropdown.value.style.top = `${selectBtnRect.height}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -283,6 +256,8 @@ const emits = defineEmits(["inp"]);
|
|||
|
||||
<template>
|
||||
<Container
|
||||
:class="[select.isOpen ? 'z-[100]' : '']"
|
||||
data-field-container
|
||||
:containerClass="`field-container ${props.containerClass}`"
|
||||
:columns="props.columns"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -86,7 +86,12 @@
|
|||
"inp_popover_multisite": "This setting is multisite.",
|
||||
"inp_popover_global": "This setting is global.",
|
||||
"inp_combobox": "Combobox input for relate select radio group.",
|
||||
"inp_combobox_no_match": "No match found.",
|
||||
"inp_combobox_no_match": "No match found",
|
||||
"inp_select_dropdown_button_desc": "Toggle hide/show radio group (dropdown) to change value.",
|
||||
"inp_select_dropdown_desc": "Radio group (dropdown) to change value.",
|
||||
"inp_input_clipboard_desc": "Copy to clipboard.",
|
||||
"inp_input_password_desc": "Toggle hide/show password.",
|
||||
"inp_combobox_placeholder": "Search",
|
||||
"action_send": "send {name}",
|
||||
"action_start": "start {name}",
|
||||
"action_disable": "disable {name}",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import Input from "@components/Forms/Field/Input.vue";
|
|||
import Datepicker from "@components/Forms/Field/Datepicker.vue";
|
||||
import Combobox from "@components/Forms/Field/Combobox.vue";
|
||||
import Button from "@components/Widget/Button.vue";
|
||||
import DashboardLayout from "@components/Dashboard/Layout.vue";
|
||||
import GridLayout from "@components/Widget/GridLayout.vue";
|
||||
import Grid from "@components/Widget/Grid.vue";
|
||||
|
||||
const checkboxData = {
|
||||
id: "test-checkbox",
|
||||
|
|
@ -27,6 +30,7 @@ const selectData = {
|
|||
requiredValues: ["no"],
|
||||
label: "Test select",
|
||||
tabId: "1",
|
||||
columns: { pc: 12, tablet: 12, mobile: 12 },
|
||||
};
|
||||
|
||||
const inputData = {
|
||||
|
|
@ -39,6 +43,7 @@ const inputData = {
|
|||
label: "Test input",
|
||||
pattern: "(test)",
|
||||
tabId: "1",
|
||||
columns: { pc: 12, tablet: 12, mobile: 12 },
|
||||
};
|
||||
|
||||
const datepickerData = {
|
||||
|
|
@ -104,27 +109,30 @@ const buttonOnlySVGData = {
|
|||
const comboboxData = {
|
||||
id: "test-combobox",
|
||||
value: "yes",
|
||||
values: ["yes", "no"],
|
||||
values: ["yes", "no", "tes", "f", "fes", "esd", "fesfse"],
|
||||
name: "test-combobox",
|
||||
disabled: false,
|
||||
required: false,
|
||||
requiredValues: ["no"],
|
||||
label: "Test combobox",
|
||||
tabId: "1",
|
||||
columns: { pc: 12, tablet: 12, mobile: 12 },
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-secondary flex flex-col items-center justify-center h-full">
|
||||
<div style="width: 300px">
|
||||
<Checkbox v-bind="checkboxData" />
|
||||
<Select v-bind="selectData" />
|
||||
<Input v-bind="inputData" />
|
||||
<Datepicker v-bind="datepickerData" />
|
||||
<Combobox v-bind="comboboxData" />
|
||||
<Button v-bind="buttonData" />
|
||||
<Button v-bind="buttonSVGData" />
|
||||
<Button v-bind="buttonOnlySVGData" />
|
||||
</div>
|
||||
</div>
|
||||
<DashboardLayout>
|
||||
<GridLayout :columns="{ pc: 4, tablet: 6, mobile: 12 }">
|
||||
<!-- widget grid -->
|
||||
<Grid>
|
||||
<Input v-bind="inputData" />
|
||||
</Grid>
|
||||
</GridLayout>
|
||||
<GridLayout :columns="{ pc: 4, tablet: 6, mobile: 12 }">
|
||||
<!-- widget grid -->
|
||||
<Grid>
|
||||
<Combobox v-bind="comboboxData" />
|
||||
</Grid>
|
||||
</GridLayout>
|
||||
</DashboardLayout>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in a new issue