Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[aform] add adropdown features #236

Merged
merged 10 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 42 additions & 50 deletions aform/src/components/form/ADropdown.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<div class="autocomplete" :class="{ isOpen: isOpen }">
<div ref="autocomplete" class="autocomplete" :class="{ isOpen: isOpen }">
<div class="input-wrapper">
<input
ref="mopInput"
ref="adropdown"
type="text"
@input="onChange"
@focus="onChange"
Expand All @@ -17,7 +17,7 @@
v-else
v-for="(result, i) in results"
:key="i"
@click="setResult(result)"
@click.stop="setResult(result)"
class="autocomplete-result"
:class="{ 'is-active': i === arrowCounter }">
{{ result }}
Expand All @@ -29,86 +29,84 @@
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref /* useTemplateRef */ } from 'vue'

const { label, items, isAsync } = defineProps<{
import { onMounted, onUnmounted, ref } from 'vue'
const { label, items, isAsync, filterFunction } = defineProps<{
label: string
items?: string[]
isAsync?: boolean
filterFunction?: (search: string) => Promise<string[]>
}>()

const emit = defineEmits(['filterChanged'])

const autocomplete = ref<HTMLElement | null>(null)
const results = ref(items)
const search = defineModel<string>()
const isLoading = ref(false)
const arrowCounter = ref(0)
const isOpen = ref(false)
// const mopInput = useTemplateRef<HTMLInputElement>('mopInput')

onMounted(() => {
document.addEventListener('click', handleClickOutside)
filterResults()
})

onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})

const setResult = result => {
search.value = result
closeResults()
}

const filterResults = () => {
if (!search.value) {
results.value = items
} else {
results.value = items.filter(item => {
return item.toLowerCase().indexOf(search.value.toLowerCase()) > -1
})
const handleClickOutside = (event: MouseEvent) => {
if (autocomplete.value && !autocomplete.value.contains(event.target as Node)) {
closeResults()
arrowCounter.value = 0
}
}

const onChange = () => {
const onChange = async () => {
isOpen.value = true
if (isAsync) {
if (isAsync && filterFunction) {
isLoading.value = true
emit('filterChanged', search.value)
try {
const filteredResults = await filterFunction(search.value)
results.value = filteredResults
} catch (error) {
console.error('Error en el filtrado asíncrono:', error)
} finally {
isLoading.value = false
}
} else {
filterResults()
}
}

const handleClickOutside = () => {
closeResults()
arrowCounter.value = 0
const setResult = (result: string) => {
search.value = result
closeResults(result)
}

const closeResults = () => {
const closeResults = (result?: string) => {
isOpen.value = false

// TODO: (test) when would this occur? how should this be tested?
if (!items.includes(search.value)) {
if (!items.includes(result || search.value)) {
search.value = ''
}
}

const filterResults = () => {
if (!search.value) {
results.value = items
} else {
results.value = items.filter(item => item.toLowerCase().includes(search.value.toLowerCase()))
}
}

const onArrowDown = () => {
if (arrowCounter.value < results.value.length) {
arrowCounter.value = arrowCounter.value + 1
arrowCounter.value += 1
}
}

const onArrowUp = () => {
if (arrowCounter.value > 0) {
arrowCounter.value = arrowCounter.value - 1
arrowCounter.value -= 1
}
}

const onEnter = () => {
search.value = results.value[arrowCounter.value]
closeResults()
closeResults(results.value[arrowCounter.value])
arrowCounter.value = 0
}

Expand All @@ -124,15 +122,12 @@ const onEnter = () => {
.autocomplete {
position: relative;
}

.input-wrapper {
min-width: 40ch;
border: 1px solid transparent;
padding: 0rem;
margin: 0rem;
margin-right: 1ch;
}

input {
width: calc(100% - 1ch);
outline: 1px solid transparent;
Expand All @@ -142,13 +137,11 @@ input {
min-height: 1.15rem;
border-radius: 0.25rem;
}

input:focus {
border: 1px solid var(--sc-input-active-border-color);
border-radius: 0.25rem 0.25rem 0 0;
border-bottom: none;
}

label {
display: block;
min-height: 1.15rem;
Expand All @@ -163,29 +156,28 @@ label {
margin: calc(-1.5rem - calc(2.15rem / 2)) 0 0 1ch;
padding: 0 0.25ch 0 0.25ch;
}

.autocomplete-results {
position: absolute;
width: calc(100% - 1ch + 1.5px);
z-index: 1;
z-index: 999;
padding: 0;
margin: 0;
color: #000000;
color: var(--sc-input-active-border-color);
border: 1px solid var(--sc-input-active-border-color);
border-radius: 0 0 0.25rem 0.25rem;
border-top: none;
background-color: #fff;
}

.autocomplete-result {
list-style: none;
text-align: left;
padding: 4px 6px;
cursor: pointer;
border-bottom: 0.5px solid lightgray;
}

.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: var(--sc-row-color-zebra-light);
color: #000000;
color: var(--sc-input-active-border-color);
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@stonecrop/aform",
"comment": "allow external clicks, fix on result selection and clear search when necesary\"",
"type": "none"
}
],
"packageName": "@stonecrop/aform"
}
19 changes: 11 additions & 8 deletions examples/aform/dropdown.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
v-model="async_dropdown_data.value"
:label="async_dropdown_data.label"
:isAsync="true"
@filterChanged="val => filterItems(val)" />
:filterFunction="filterItems" />
</div>
</Story>
</template>
Expand All @@ -32,21 +32,24 @@ const async_dropdown_data = reactive({
label: 'Animals',
})

function filterItems(search: string) {
setTimeout(() => {
async_dropdown_data.items = async_dropdown_data.allItems.filter(item => {
return item.toLowerCase().indexOf(search.toLowerCase()) > -1
})
}, 750)
function filterItems(search: string): Promise<string[]> {
return new Promise(resolve => {
setTimeout(() => {
const filtered = async_dropdown_data.allItems.filter(item => item.toLowerCase().includes(search.toLowerCase()))
async_dropdown_data.items = filtered
resolve(filtered)
}, 750)
})
}
</script>

<style>
.dropdown-form {
min-height: 60px;
height: 200px;
display: flex;
flex-direction: row;
align-items: center;
align-items: top;
margin: 0px;
padding-left: 1ch;
padding-right: 1ch;
Expand Down
Loading