feat: created autocomplete component to control options with up/down key
This commit is contained in:
parent
456cdae3b0
commit
d95b3b6fb0
7 changed files with 168 additions and 101 deletions
|
|
@ -41,7 +41,7 @@ onMounted(() => store.fetch());
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="drop-down-area" />
|
||||
<div id="autocomplete-area" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ defineExpose({ focus_on_label });
|
|||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue