feat: created autocomplete component to control options with up/down key

This commit is contained in:
Shariq Ansari 2023-10-31 20:28:31 +05:30
parent 456cdae3b0
commit d95b3b6fb0
7 changed files with 168 additions and 101 deletions

View file

@ -41,7 +41,7 @@ onMounted(() => store.fetch());
</div>
</div>
</div>
<div id="drop-down-area" />
<div id="autocomplete-area" />
</template>
<style lang="scss" scoped>

View file

@ -3,14 +3,22 @@
ref="add_field_btn_ref"
class="add-field-btn btn btn-xs btn-icon"
:title="__('Add field')"
@click="toggle_fieldtype_dropdown"
@click.stop="toggle_fieldtype_options"
>
<slot>
{{ __("Add a field") }}
</slot>
<Teleport to="#drop-down-area">
<div class="drop-down" ref="dropdown_ref">
<Autocomplete v-if="show" :items="fields" v-model="search_text" />
<Teleport to="#autocomplete-area">
<div class="autocomplete" ref="autocomplete_ref">
<div v-show="show">
<Autocomplete
v-model:show="show"
:value="autocomplete_value"
:options="fields"
@change="add_new_field"
placeholder="Search fieldtypes..."
/>
</div>
</div>
</Teleport>
</button>
@ -22,6 +30,7 @@ import { useStore } from "../store";
import { clone_field } from "../utils";
import { createPopper } from "@popperjs/core";
import { computed, nextTick, ref, watch } from "vue";
import { onClickOutside } from "@vueuse/core";
const store = useStore();
@ -36,7 +45,7 @@ const props = defineProps({
},
});
const emit = defineEmits(["update:modelValue", "update_parent"]);
const emit = defineEmits(["update:modelValue"]);
const selected = computed(() => {
let fieldname = props.field ? props.field.df.name : props.column.df.name;
@ -44,46 +53,31 @@ const selected = computed(() => {
});
const show = ref(false);
const search_text = ref("");
const autocomplete_value = ref("");
const fields = computed(() => {
let fields = frappe.model.all_fieldtypes
.filter((df) => {
if (in_list(frappe.model.layout_fields, df)) {
return false;
}
if (search_text.value) {
if (df.toLowerCase().includes(search_text.value.toLowerCase())) {
return true;
}
return false;
} else {
return true;
}
return true;
})
.map((df) => {
let out = {
label: df,
onClick: () => {
let new_field = {
df: store.get_df(df),
table_columns: [],
};
add_new_field(clone_field(new_field));
},
};
let out = { label: df };
return out;
});
return [...fields];
});
const add_field_btn_ref = ref(null);
const dropdown_ref = ref(null);
const autocomplete_ref = ref(null);
const popper = ref(null);
onClickOutside(add_field_btn_ref, () => (show.value = false), { ignore: [autocomplete_ref] });
function setupPopper() {
if (!popper.value) {
popper.value = createPopper(add_field_btn_ref.value, dropdown_ref.value, {
popper.value = createPopper(add_field_btn_ref.value, autocomplete_ref.value, {
placement: "bottom-start",
modifiers: [
{
@ -99,22 +93,32 @@ function setupPopper() {
}
}
function toggle_fieldtype_dropdown() {
function toggle_fieldtype_options() {
show.value = !show.value;
search_text.value = "";
autocomplete_value.value = "";
nextTick(() => setupPopper());
}
function add_new_field(field) {
fieldtype = field?.label;
if (!fieldtype) return;
let new_field = {
df: store.get_df(fieldtype),
table_columns: [],
};
let cloned_field = clone_field(new_field);
// insert new field after current field
let index = 0;
if (props.field) {
index = props.column.fields.indexOf(props.field);
}
props.column.fields.splice(index + 1, 0, field);
store.form.selected_field = field.df;
props.column.fields.splice(index + 1, 0, cloned_field);
store.form.selected_field = cloned_field.df;
show.value = false;
emit("update_parent");
}
watch(selected, (val) => {
@ -123,7 +127,7 @@ watch(selected, (val) => {
</script>
<style lang="scss" scoped>
.drop-down {
.autocomplete {
z-index: 100;
}
</style>

View file

@ -1,97 +1,159 @@
<template>
<div class="drop-down">
<div class="search-box">
<input
ref="searchInput"
class="search-input form-control"
type="text"
:placeholder="__('Search field...')"
@input="(event) => $emit('update:modelValue', event.target.value)"
:value="modelValue"
@click.stop
/>
<span class="search-icon">
<div v-html="frappe.utils.icon('search', 'sm')"></div>
</span>
</div>
<div class="drop-down-list">
<button
class="btn drop-down-item"
v-for="(item, i) in items"
:key="i"
@click.stop="(i) => item.onClick(i)"
>
<slot>{{ item.label }}</slot>
</button>
</div>
</div>
<Combobox v-model="selectedValue" nullable>
<ComboboxOptions class="combo-box-options" static>
<div class="search-box">
<ComboboxInput
ref="search"
class="search-input form-control"
type="text"
@change="(e) => (query = e.target.value)"
:value="query"
:placeholder="props.placeholder"
autocomplete="off"
@click.stop
/>
<button class="clear-button btn btn-sm" @click="clear_search">
<div v-html="frappe.utils.icon('close', 'sm')" />
</button>
</div>
<div class="combo-box-items">
<ComboboxOption
as="template"
v-for="(field, i) in filteredOptions"
:key="i"
:value="field"
v-slot="{ active }"
>
<li :class="['combo-box-option', active ? 'active' : '']">
{{ field.label }}
</li>
</ComboboxOption>
</div>
</ComboboxOptions>
</Combobox>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { Combobox, ComboboxInput, ComboboxOptions, ComboboxOption } from "@headlessui/vue";
import { computed, ref, useAttrs, watch, nextTick } from "vue";
const props = defineProps({
items: {
options: {
type: Array,
required: true,
default: [],
},
placeholder: {
type: String,
default: "",
},
modelValue: {
type: String,
default: "",
},
show: {
type: Boolean,
default: false,
},
});
const searchInput = ref(null);
const emit = defineEmits(["update:modelValue", "update:show", "change"]);
const attrs = useAttrs();
onMounted(() => {
searchInput.value.focus();
const query = ref(null);
const search = ref(null);
const showOptions = computed({
get() {
return props.show;
},
set(val) {
emit("update:show", val);
},
});
const selectedValue = computed({
get() {
return attrs.value;
},
set(val) {
query.value = "";
if (val) {
showOptions.value = false;
}
emit("change", val);
},
});
const filteredOptions = computed(() => {
return query.value
? props.options.filter((option) => {
return option.label.toLowerCase().includes(query.value.toLowerCase());
})
: props.options;
});
function clear_search() {
selectedValue.value = "";
search.value.el.focus();
}
watch(showOptions, (val) => {
if (val) {
nextTick(() => {
search.value.el.focus();
});
}
});
</script>
<style lang="scss" scoped>
.drop-down {
display: inline-block;
background-color: var(--fg-color);
box-shadow: var(--shadow-base) !important;
border-radius: var(--border-radius-sm);
top: 30px;
right: 0;
width: 170px;
z-index: 99999999;
.combo-box {
z-index: 100;
}
.drop-down-list {
overflow-y: auto;
max-height: 250px;
text-align: left;
padding: 0 6px 6px;
.combo-box-options {
width: 100%;
background-color: var(--white);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-2xl);
padding: 0;
}
.drop-down-item {
.combo-box-option {
font-size: small;
text-align: left;
border-radius: var(--border-radius-sm);
margin: 1px 0px;
padding: 6px 10px;
width: 100%;
&:hover {
&:hover,
&.active {
background-color: var(--bg-light-gray);
}
}
.search-box {
padding: 6px;
.search-input {
padding-left: 30px;
font-size: small;
width: 100% !important;
background-color: var(--control-bg) !important;
}
.combo-box-items {
max-height: 200px;
padding: 5px;
padding-top: 0px;
overflow-y: auto;
}
.search-icon {
.search-box {
position: relative;
padding: 6px;
.clear-button {
position: absolute;
left: 13px;
top: 8px;
right: 0;
top: 0;
bottom: 0;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.search-input {
width: 100%;
}
}
</style>

View file

@ -72,7 +72,6 @@ defineExpose({ focus_on_label });
&:focus {
outline: none;
border-radius: var(--border-radius);
background-color: inherit;
}

View file

@ -93,11 +93,7 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
</template>
<template #actions>
<div class="field-actions" :hidden="store.read_only">
<AddFieldButton
:column="column"
:field="field"
@update_parent="hovered = false"
>
<AddFieldButton :column="column" :field="field">
<div v-html="frappe.utils.icon('add', 'sm')" />
</AddFieldButton>
<button

View file

@ -20,9 +20,10 @@
},
"homepage": "https://frappeframework.com",
"dependencies": {
"@popperjs/core": "^2.11.2",
"@editorjs/editorjs": "^2.26.3",
"@frappe/esbuild-plugin-postcss2": "^0.1.3",
"@headlessui/vue": "^1.7.16",
"@popperjs/core": "^2.11.2",
"@redis/client": "^1.5.8",
"@vue-flow/background": "^1.1.0",
"@vue-flow/core": "^1.16.2",

View file

@ -55,6 +55,11 @@
stylus "^0.x"
tmp "^0.2.1"
"@headlessui/vue@^1.7.16":
version "1.7.16"
resolved "https://registry.yarnpkg.com/@headlessui/vue/-/vue-1.7.16.tgz#bdc9d32d329248910325539b99e6abfce0c69f89"
integrity sha512-nKT+nf/q6x198SsyK54mSszaQl/z+QxtASmgMEJtpxSX2Q0OPJX0upS/9daDyiECpeAsvjkoOrm2O/6PyBQ+Qg==
"@jridgewell/sourcemap-codec@^1.4.13":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"