mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-11-01 09:07:21 +08:00
Implement basic select/search-select components in vue [SCI-8249]
This commit is contained in:
parent
1457d09da8
commit
863054b044
4 changed files with 261 additions and 0 deletions
101
app/assets/stylesheets/shared/select.scss
Normal file
101
app/assets/stylesheets/shared/select.scss
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
.sn-select {
|
||||
border: 1px solid;
|
||||
border-color: var(--sn-grey);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: .5em;
|
||||
position: relative;
|
||||
|
||||
&.sn-select--blank {
|
||||
.sn-select__value {
|
||||
color: var(--sn-grey);
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
&.sn-select--search.sn-select--open {
|
||||
.sn-select__value {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sn-select__value {
|
||||
all: unset;
|
||||
overflow: hidden;
|
||||
padding-right: 1em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--sn-sleepy-grey);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--sn-sleepy-grey);
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
|
||||
.caret {
|
||||
border-top-color: var(--sn-grey);
|
||||
}
|
||||
}
|
||||
|
||||
.sn-select__options {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sn-select__caret {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: 1.1em;
|
||||
}
|
||||
|
||||
.sn-select__search-input {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
display: none;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.sn-select--open {
|
||||
background: $color-white;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-color: var(--sn-science-blue);
|
||||
|
||||
.sn-select__search-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sn-select__caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sn-select__options {
|
||||
background: var(--sn-white);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: $flyout-shadow;
|
||||
display: block;
|
||||
left: 0;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
top: 2.5em;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.sn-select__option {
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.sn-select__option:hover {
|
||||
background: var(--sn-blue);
|
||||
color: var(--sn-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
app/javascript/vue/shared/select.vue
Normal file
82
app/javascript/vue/shared/select.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div @click="toggle" ref="container" class="sn-select" :class="{ 'sn-select--open': isOpen, 'sn-select--blank': !valueLabel, 'disabled': disabled }">
|
||||
<slot>
|
||||
<button ref="focusElement" class="sn-select__value">
|
||||
<span>{{ valueLabel || (placeholder || i18n.t('general.select')) }}</span>
|
||||
</button>
|
||||
<span class="sn-select__caret caret"></span>
|
||||
</slot>
|
||||
<div ref="optionsContainer" class="sn-select__options" :style="optionPositionStyle">
|
||||
<div v-for="option in options" :key="option[0]" @click="setValue(option[0])" class="sn-select__option">
|
||||
{{ option[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Select',
|
||||
props: {
|
||||
options: { type: Array, default: () => [] },
|
||||
initialValue: { type: [String, Number] },
|
||||
placeholder: { type: String },
|
||||
disabled: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: null,
|
||||
isOpen: false,
|
||||
optionPositionStyle: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
valueLabel() {
|
||||
let option = this.options.find((o) => o[0] === this.value);
|
||||
return option && option[1];
|
||||
},
|
||||
focusElement() {
|
||||
return this.$refs.focusElement || this.$scopedSlots.default()[0].context.$refs.focusElement;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.focusElement.onblur = this.blur;
|
||||
document.addEventListener("scroll", this.updateOptionPosition);
|
||||
},
|
||||
methods: {
|
||||
blur() {
|
||||
setTimeout(() => {
|
||||
this.isOpen = false;
|
||||
this.$emit('blur');
|
||||
}, 100)
|
||||
},
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
|
||||
if (this.isOpen) {
|
||||
this.$emit('open');
|
||||
this.$nextTick(() => {
|
||||
this.focusElement.focus();
|
||||
});
|
||||
this.$refs.optionsContainer.scrollTop = 0;
|
||||
this.updateOptionPosition();
|
||||
} else {
|
||||
this.optionPositionStyle = '';
|
||||
this.$emit('close');
|
||||
}
|
||||
},
|
||||
setValue(value) {
|
||||
this.value = value;
|
||||
this.$emit('change', this.value);
|
||||
},
|
||||
updateOptionPosition() {
|
||||
let rect = this.$refs.container.getBoundingClientRect();
|
||||
let top =rect.top + rect.height;
|
||||
let left = rect.left;
|
||||
let width = rect.width;
|
||||
|
||||
this.optionPositionStyle = `position: fixed; top: ${top}px; left: ${left}px; width: ${width}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
77
app/javascript/vue/shared/select_search.vue
Normal file
77
app/javascript/vue/shared/select_search.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<Select class="sn-select--search" :options="currentOptions" :placeholder="placeholder" @change="change" @blur="blur" @open="open" @close="close">
|
||||
<input ref="focusElement" v-model="query" type="text" class="sn-select__search-input" :placeholder="searchPlaceholder" />
|
||||
<span class="sn-select__value">{{ valueLabel || (placeholder || i18n.t('general.select')) }}</span>
|
||||
<span class="sn-select__caret caret"></span>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select from './select.vue'
|
||||
|
||||
export default {
|
||||
name: 'SelectSearch',
|
||||
props: {
|
||||
options: { type: Array, default: () => [] },
|
||||
optionsUrl: { type: String },
|
||||
placeholder: { type: String },
|
||||
searchPlaceholder: { type: String }
|
||||
},
|
||||
components: { Select },
|
||||
data() {
|
||||
return {
|
||||
value: null,
|
||||
query: null,
|
||||
currentOptions: null,
|
||||
isOpen: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.currentOptions = this.options;
|
||||
},
|
||||
watch: {
|
||||
query() {
|
||||
if(!this.query) {
|
||||
this.currentOptions = this.options;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.optionsUrl) {
|
||||
this.fetchOptions();
|
||||
} else {
|
||||
this.currentOptions = this.options.filter((o) => o[1].toLowerCase().includes(this.query.toLowerCase()));
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
valueLabel() {
|
||||
let option = this.options.find((o) => o[0] === this.value);
|
||||
return option && option[1];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
blur() {
|
||||
this.$emit('blur');
|
||||
},
|
||||
change(value) {
|
||||
this.value = value;
|
||||
this.$emit('change', this.value);
|
||||
},
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
this.$emit('open');
|
||||
},
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.$emit('close');
|
||||
},
|
||||
fetchOptions() {
|
||||
$.get(`${this.optionsUrl}?query=${this.query}`,
|
||||
(data) => {
|
||||
this.currentOptions = data;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -3367,6 +3367,7 @@ en:
|
|||
remove: "Remove"
|
||||
clone_label: "Clone"
|
||||
download: "Download"
|
||||
select: "Select"
|
||||
# In order to use the strings 'yes' and 'no' as keys, you need to wrap them with quotes
|
||||
'yes': "Yes"
|
||||
'no': "No"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue