1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-02-01 18:06:53 +01:00

feat(UI): ForeignKeySelect implements BaseSelect component

This commit is contained in:
Giulio Ganci 2022-05-09 11:29:25 +02:00
parent 0043d07708
commit 302c66457d
6 changed files with 147 additions and 100 deletions

View File

@ -6,7 +6,7 @@
role="combobox" role="combobox"
:tabindex="searchable ? -1 : tabindex" :tabindex="searchable ? -1 : tabindex"
@focus="activate()" @focus="activate()"
@blur="searchable ? false : deactivate()" @blur="searchable ? false : handleBlurEvent()"
@keyup.esc="deactivate()" @keyup.esc="deactivate()"
@keydown.self.down.prevent="moveDown()" @keydown.self.down.prevent="moveDown()"
@keydown.self.up.prevent="moveUp" @keydown.self.up.prevent="moveUp"
@ -23,8 +23,8 @@
:tabindex="tabindex" :tabindex="tabindex"
:value="searchText" :value="searchText"
@input="searchText = $event.target.value" @input="searchText = $event.target.value"
@focus.prevent="activate()" @focus.prevent="!isOpen ? activate() : false"
@blur.prevent="deactivate()" @blur.prevent="handleBlurEvent()"
@keyup.esc="deactivate()" @keyup.esc="deactivate()"
@keydown.down.prevent="keyArrows('down')" @keydown.down.prevent="keyArrows('down')"
@keydown.up.prevent="keyArrows('up')" @keydown.up.prevent="keyArrows('up')"
@ -32,10 +32,14 @@
> >
<span v-if="searchable && !isOpen || !searchable">{{ currentOptionLabel }}</span> <span v-if="searchable && !isOpen || !searchable">{{ currentOptionLabel }}</span>
</div> </div>
<div <Teleport
v-if="isOpen" v-if="isOpen"
ref="teleportEl"
:to="dropdownContainer"
>
<div
ref="optionList" ref="optionList"
class="select__list-wrapper" :class="`select__list-wrapper ${dropdownClass ? dropdownClass : '' }`"
> >
<ul class="select__list" @mousedown.prevent> <ul class="select__list" @mousedown.prevent>
<li <li
@ -59,11 +63,12 @@
</li> </li>
</ul> </ul>
</div> </div>
</Teleport>
</div> </div>
</template> </template>
<script> <script>
import { defineComponent, computed, ref, watch } from 'vue'; import { defineComponent, computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'BaseSelect', name: 'BaseSelect',
@ -71,6 +76,9 @@ export default defineComponent({
modelValue: { modelValue: {
type: [String, Number, Object] type: [String, Number, Object]
}, },
value: {
type: [String, Number, Object]
},
searchable: { searchable: {
type: Boolean, type: Boolean,
default: true default: true
@ -89,21 +97,35 @@ export default defineComponent({
}, },
optionTrackBy: { optionTrackBy: {
type: [String, Function], type: [String, Function],
default: (opt) => opt.id || opt.value default: () => (opt) => {
for (const guess of ['id', 'value']) if (opt[guess]) return guess;
}
}, },
optionLabel: { optionLabel: {
type: [String, Function], type: [String, Function],
default: (opt) => opt.label default: () => (opt) => opt.label ? 'label' : undefined
}, },
closeOnSelect: { closeOnSelect: {
type: Boolean, type: Boolean,
default: true default: true
},
dropdownContainer: {
type: String,
default: '#main-content'
},
dropdownOffsets: {
type: Object,
default: () => ({ top: 10, left: 0 })
},
dropdownClass: {
type: String
} }
}, },
emits: ['select', 'open', 'close', 'update:modelValue'], emits: ['select', 'open', 'close', 'update:modelValue', 'change', 'blur'],
setup (props, { emit }) { setup (props, { emit }) {
const hightlightedIndex = ref(0); const hightlightedIndex = ref(0);
const isOpen = ref(false); const isOpen = ref(false);
const internalValue = ref(props.modelValue || props.value);
const el = ref(null); const el = ref(null);
const searchInput = ref(null); const searchInput = ref(null);
const optionList = ref(null); const optionList = ref(null);
@ -134,12 +156,12 @@ export default defineComponent({
}); });
const getOptionValue = (opt) => { const getOptionValue = (opt) => {
const key = typeof props.optionTrackBy === 'function' ? props.optionTrackBy() : props.optionTrackBy; const key = typeof props.optionTrackBy === 'function' ? props.optionTrackBy(opt) : props.optionTrackBy;
return key ? opt[key] : opt; return key ? opt[key] : opt;
}; };
const getOptionLabel = (opt) => { const getOptionLabel = (opt) => {
const key = typeof props.optionLabel === 'function' ? props.optionLabel() : props.optionLabel; const key = typeof props.optionLabel === 'function' ? props.optionLabel(opt) : props.optionLabel;
return key ? opt[key] : opt; return key ? opt[key] : opt;
}; };
@ -153,19 +175,22 @@ export default defineComponent({
}); });
const select = (opt) => { const select = (opt) => {
internalValue.value = opt;
emit('select', opt); emit('select', opt);
emit('update:modelValue', getOptionValue(opt)); emit('update:modelValue', getOptionValue(opt));
emit('change', opt);
if (props.closeOnSelect) if (props.closeOnSelect)
deactivate(); deactivate();
}; };
const isSelected = (opt) => { const isSelected = (opt) => {
return props.modelValue === getOptionValue(opt); return internalValue.value === getOptionValue(opt);
}; };
const activate = () => { const activate = () => {
if (isOpen.value) return; if (isOpen.value) return;
isOpen.value = true;
if (props.searchable) if (props.searchable)
searchInput.value.focus(); searchInput.value.focus();
@ -173,14 +198,13 @@ export default defineComponent({
else else
el.value.focus(); el.value.focus();
isOpen.value = true; nextTick(() => adjustListPosition());
emit('open'); emit('open');
}; };
const deactivate = () => { const deactivate = () => {
if (!isOpen.value) return; if (!isOpen.value) return;
isOpen.value = false; isOpen.value = false;
if (props.searchable) if (props.searchable)
@ -194,6 +218,16 @@ export default defineComponent({
emit('close'); emit('close');
}; };
const adjustListPosition = () => {
const element = el.value;
const { left, top } = element.getBoundingClientRect();
const { left: offsetLeft = 0, top: offsetTop = 0 } = props.dropdownOffsets;
optionList.value.style.left = `${left + offsetLeft}px`;
optionList.value.style.top = `${top + element.clientHeight + offsetTop}px`;
optionList.value.style.minWidth = `${element.clientWidth}px`;
};
const keyArrows = (direction) => { const keyArrows = (direction) => {
const sum = direction === 'down' ? +1 : -1; const sum = direction === 'down' ? +1 : -1;
const index = hightlightedIndex.value + sum; const index = hightlightedIndex.value + sum;
@ -211,6 +245,18 @@ export default defineComponent({
optionList.value.scrollTop = optEl.offsetTop - optionList.value.clientHeight + optEl.clientHeight; optionList.value.scrollTop = optEl.offsetTop - optionList.value.clientHeight + optEl.clientHeight;
}; };
const handleBlurEvent = () => {
deactivate();
emit('blur');
};
onMounted(() => {
window.addEventListener('resize', adjustListPosition);
});
onUnmounted(() => {
window.removeEventListener('resize', adjustListPosition);
});
return { return {
el, el,
searchInput, searchInput,
@ -228,7 +274,8 @@ export default defineComponent({
isOpen, isOpen,
hightlightedIndex, hightlightedIndex,
optionList, optionList,
optionRefs optionRefs,
handleBlurEvent
}; };
} }
}); });
@ -236,7 +283,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.select { .select {
position: relative;
display: block; display: block;
&__search-input { &__search-input {
@ -248,10 +295,9 @@ export default defineComponent({
} }
&__list-wrapper { &__list-wrapper {
position: absolute; position: fixed;
display: block; display: block;
width: 100%; z-index: 5;
z-index: 50;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
max-height: 240px; max-height: 240px;
overflow: auto; overflow: auto;

View File

@ -1,23 +1,15 @@
<template> <template>
<select <BaseSelect
ref="editField" ref="editField"
:options="foreigns"
class="form-select pl-1 pr-4" class="form-select pl-1 pr-4"
:class="{'small-select': size === 'small'}" :class="{'small-select': size === 'small'}"
:value="currentValue"
dropdown-class="select-sm"
dropdown-container=".workspace-query-results > .vscroll"
@change="onChange" @change="onChange"
@blur="$emit('blur')" @blur="$emit('blur')"
> />
<option v-if="!isValidDefault" :value="modelValue">
{{ modelValue === null ? 'NULL' : modelValue }}
</option>
<option
v-for="row in foreignList"
:key="row.foreign_column"
:value="row.foreign_column"
:selected="row.foreign_column === modelValue"
>
{{ row.foreign_column }} {{ cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '') }}
</option>
</select>
</template> </template>
<script> <script>
@ -26,8 +18,11 @@ import Tables from '@/ipc-api/Tables';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { TEXT, LONG_TEXT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT } from 'common/fieldTypes';
import BaseSelect from '@/components/BaseSelect.vue';
export default { export default {
name: 'ForeignKeySelect', name: 'ForeignKeySelect',
components: { BaseSelect },
props: { props: {
modelValue: [String, Number], modelValue: [String, Number],
keyUsage: Object, keyUsage: Object,
@ -47,7 +42,8 @@ export default {
}, },
data () { data () {
return { return {
foreignList: [] foreignList: [],
currentValue: this.modelValue
}; };
}, },
computed: { computed: {
@ -55,6 +51,17 @@ export default {
if (!this.foreignList.length) return true; if (!this.foreignList.length) return true;
if (this.modelValue === null) return false; if (this.modelValue === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.modelValue.toString()); return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.modelValue.toString());
},
foreigns () {
const list = [];
if (!this.isValidDefault)
list.push({ value: this.modelValue, label: this.modelValue === null ? 'NULL' : this.modelValue });
for (const row of this.foreignList)
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${this.cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '')}` });
return list;
} }
}, },
async created () { async created () {
@ -95,8 +102,8 @@ export default {
} }
}, },
methods: { methods: {
onChange () { onChange (opt) {
this.$emit('update:modelValue', this.$refs.editField.value); this.$emit('update:modelValue', opt.value);
}, },
cutText (val) { cutText (val) {
if (typeof val !== 'string') return val; if (typeof val !== 'string') return val;

View File

@ -56,6 +56,8 @@
option-track-by="slug" option-track-by="slug"
option-label="name" option-label="name"
class="form-select" class="form-select"
dropdown-container=".workspace .connection-panel-wrapper"
:dropdown-offsets="{top: 10}"
/> />
</div> </div>
</div> </div>

View File

@ -302,11 +302,7 @@ option:checked {
border-color: $primary-color !important; border-color: $primary-color !important;
@include control-shadow(); @include control-shadow();
} }
}
.select__list-wrapper {
border: 1px solid transparent;
border-radius: $border-radius;
box-shadow: 0px 8px 17px 0px rgba(0, 0, 0, 0.2), 0px 6px 20px 0px rgba(0, 0, 0, 0.19);
} }
.select__list { .select__list {
@ -314,17 +310,19 @@ option:checked {
li { li {
margin: 0; margin: 0;
padding: 0.3rem 0.8rem; padding: 0.3rem 0.8rem;
}
}
&.select-sm { .select-sm & {
.select__list {
li {
padding: 0.05rem 0.3rem; padding: 0.05rem 0.3rem;
} }
} }
} }
.select__list-wrapper {
border: 1px solid transparent;
border-radius: $border-radius;
box-shadow: 0px 8px 17px 0px rgba(0, 0, 0, 0.2), 0px 6px 20px 0px rgba(0, 0, 0, 0.19);
}
.select__option--selected { .select__option--selected {
background: rgba($primary-color, 0.25); background: rgba($primary-color, 0.25);
} }
@ -332,8 +330,6 @@ option:checked {
.select__option--highlight { .select__option--highlight {
background: $primary-color; background: $primary-color;
} }
}
}
.form-input[type="file"] { .form-input[type="file"] {
overflow: hidden; overflow: hidden;

View File

@ -121,14 +121,12 @@
border-color: $primary-color; border-color: $primary-color;
} }
.form-select {
.select { .select {
&__list-wrapper { &__list-wrapper {
border-color: $bg-color-gray; border-color: $bg-color-gray;
background-color: $bg-color-light-dark; background-color: $bg-color-light-dark;
} }
} }
}
.form-input[readonly] { .form-input[readonly] {
background-color: $bg-color-dark; background-color: $bg-color-dark;

View File

@ -18,7 +18,6 @@
background: #ababab; background: #ababab;
} }
.form-select {
.select { .select {
&__list-wrapper { &__list-wrapper {
border: #bcc3ce; border: #bcc3ce;
@ -29,7 +28,6 @@
color: $light-color; color: $light-color;
} }
} }
}
.menu { .menu {
.menu-item a { .menu-item a {