add item edit page

This commit is contained in:
j3d1 2024-08-30 23:05:01 +02:00
parent b8f1942ab1
commit f01d513803
12 changed files with 707 additions and 0 deletions

View file

@ -0,0 +1,204 @@
<template>
<div class="input-group">
<div class="badge-list form-control form-control-lg" style="position: relative;">
<span v-for="(option, index) in value" :key="index">
<slot :option="option" :index="index"></slot>
</span>
<input type="text" class="invisible-input" v-model="textinput" ref="input"
@keydown="keyDown">
<ul class="dropdown-menu" style="position: absolute; top: 100%; left: 0; width: 100%; z-index: 100;"
v-if="filteredOption.length > 0">
<li v-for="(option, index) in filteredOption" :key="index"
@mousedown="chooseOption(option)" @mouseover="selectOption(option)"
:class="{chosen: localValue.includes(option), selected: selectedTag === option, 'dropdown-item': true}">
{{ option }}
<b>
<b-icon-check class="checkmark"></b-icon-check>
<b-icon-x class="cross"></b-icon-x>
<b-icon-plus class="plus"></b-icon-plus>
</b>
</li>
</ul>
</div>
</div>
</template>
<style scoped>
.badge-list {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
padding: 0 .25rem;
}
.badge-list > * {
flex: 0 1 auto;
padding: .25rem 0;
}
.badge-list > input.invisible-input {
flex: 1 1 auto;
}
input.invisible-input {
border: none;
outline: none;
color: #495057;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right .7rem center;
background-size: 16px 12px;
}
.invisible-input:focus ~ .dropdown-menu {
display: block;
}
.dropdown-menu {
display: none;
overflow-x: auto;
max-height: 50vh;
}
.checkmark, .cross, .plus {
display: none;
}
.dropdown-item.selected {
background-color: var(--bs-primary);
color: var(--bs-light);
& .plus {
display: inline-block;
}
}
.dropdown-item.chosen {
& .checkmark {
display: inline-block;
}
}
.dropdown-item.chosen.selected {
background-color: var(--bs-danger);
color: var(--bs-light);
& .cross {
display: inline-block;
}
& .checkmark, & .plus {
display: none;
}
}
</style>
<script>
import * as BIcons from "bootstrap-icons-vue";
export default {
name: "BadgeSelectField",
data() {
return {
textinput: "",
selectedTag: ""
}
},
components: {
...BIcons
},
props: {
value: {
type: Array,
required: true
},
options: {
type: Array,
required: true
}
},
model: {
prop: "value",
event: "addElement"
},
computed: {
availableTags() {
return this.options.filter(option => !this.localValue.includes(option));
},
filteredOption() {
return this.options.filter(option => option.includes(this.textinput) || option === this.selectedTag);
},
localValue: {
get() {
return this.value;
},
set(value) {
}
}
},
methods: {
chooseOption(option) {
this.$emit("addElement", option);
},
resetSelection() {
this.textinput = "";
this.selectedTag = null;
},
selectOption(option) {
this.selectedTag = option;
},
keyDown(event) {
const key = event.key;
if (key === "Enter") {
event.preventDefault();
this.chooseOption(this.selectedTag);
this.$refs.input.blur();
} else if (key === "ArrowDown") {
event.preventDefault();
if (this.selectedTag === "") {
this.selectedTag = this.filteredOption[0];
} else {
const index = this.filteredOption.indexOf(this.selectedTag);
if (index < this.filteredOption.length - 1) {
this.selectedTag = this.filteredOption[index + 1];
} else {
this.selectedTag = this.filteredOption[0];
}
setTimeout(() => {
const element = this.$el.querySelector(".selected");
if (element) {
element.scrollIntoView({block: "nearest"});
}
}, 0);
}
} else if (key === "ArrowUp") {
event.preventDefault();
if (this.selectedTag === "") {
this.selectedTag = this.filteredOption[this.filteredOption.length - 1];
} else {
const index = this.filteredOption.indexOf(this.selectedTag);
if (index > 0) {
this.selectedTag = this.filteredOption[index - 1];
} else {
this.selectedTag = this.filteredOption[this.filteredOption.length - 1];
}
setTimeout(() => {
const element = this.$el.querySelector(".selected");
if (element) {
element.scrollIntoView({block: "nearest"});
}
}, 0);
}
} else if (key === "ArrowRight") {
if (event.target.selectionStart === this.textinput.length) {
event.preventDefault();
this.chooseOption(this.selectedTag);
}
} else if (key === "Escape") {
event.preventDefault();
this.$refs.input.blur();
}
}
}
}
</script>

View file

@ -0,0 +1,115 @@
<template>
<div class="input-group" :title="prettyDescription">
<label class="input-group-text form-control-inline form-control-sm text-light border-dark bg-dark badge">
{{ property.name }} =
</label>
<input type="number" min="0"
class="form-control form-control-inline form-control-sm text-light border-dark bg-dark badge"
placeholder="Enter value" @input.prevent="inputChange"
:value="localValue" :style="inputStyle"/>
<span class="input-group-text form-control-sm text-light border-dark bg-dark badge"
v-if="property.unit_symbol" :title="(property.unit_name?property.unit_name:property.unit_symbol)">
{{ property.unit_symbol }}
</span>
<span class="input-group-text form-control-sm text-light border-dark bg-dark badge pl-1">
<b-icon-x-circle @click="removeProperty()"></b-icon-x-circle>
</span>
</div>
</template>
<style scoped>
.form-control-inline {
display: inline;
width: auto;
}
.input-group {
width: initial;
}
.form-control.badge {
border: 0;
}
.input-group-text.badge {
padding: .3rem 0rem;
}
.input-group .badge:first-child {
padding-left: 0.5rem;
}
.input-group .badge:last-child {
padding-right: 0.5rem;
}
input.badge {
--width: 0em;
min-height: calc(1.2rem);
line-height: 1;
min-width: calc(var(--width) + 1.35em + 18px);
width: 0;
text-align: left;
}
input[type=number].badge {
}
input.badge:empty {
display: initial;
}
</style>
<script>
import * as BIcons from "bootstrap-icons-vue";
import {mapActions, mapState} from "vuex";
export default {
name: "PropertyBadge",
components: {
...BIcons
},
data() {
return {
localValue: this.property.value
}
},
props: {
property: {
type: Object,
required: true
}
},
property: {
prop: "property",
event: "modelChange"
},
computed: {
prettyDescription() {
var d = this.property.description ? this.property.description : this.property.name
if (this.property.unit_name) {
d += " (" + this.property.unit_name + ")"
}
return (d || "").replace(/<[^>]*>?/gm, '')
},
inputStyle() {
const w = this.property.value ? Math.max(1, this.property.value.toString().length) : 1
if (this.property.value === undefined) {
console.log("No value for property", this.property)
}
return {
"--width": w + "ex"
}
}
},
methods: {
inputChange(event) {
this.localValue = event.target.value
this.$emit("modelChange", {...this.property, value: this.localValue})
},
removeProperty() {
this.$emit("remove")
}
},
}
</script>

View file

@ -0,0 +1,89 @@
<template>
<div>
<badge-select-field :value="this.splicedValue" :options="this.properties.map(p => p.name)" @addElement="addProperty"
ref="badgeSelect">
<template v-slot:default="{option, index}">
<PropertyBadge :key="index"
:property="option"
@modelChange="setValue(index, $event)"
@remove="removeProperty(index)"/>
</template>
</badge-select-field>
</div>
</template>
<style scoped>
</style>
<script>
import * as BIcons from "bootstrap-icons-vue";
import {mapActions, mapState} from "vuex";
import PropertyBadge from "@/components/PropertyBadge.vue";
import BadgeSelectField from "@/components/BadgeSelectField.vue";
export default {
name: "PropertyField",
components: {
BadgeSelectField,
PropertyBadge,
...BIcons
},
props: {
value: {
type: Array,
required: true
}
},
model: {
prop: "value",
event: "input"
},
computed: {
...mapState(["properties"]),
availableProperties() {
if (!this.properties) return [];
if (!this.value) return this.properties;
return this.properties.filter(property => !this.value.map(p => p.name).includes(property.name));
},
localValue: {
get() {
if (!this.value) return [];
return this.value;
},
set(value) {
this.$emit("input", value);
}
},
splicedValue() {
if (!this.value || !this.properties) return [];
return this.value.map(property => {
return {
...this.properties.find(p => p.name === property.name),
value: property.value
}
})
}
},
methods: {
...mapActions(["fetchProperties"]),
addProperty(property) {
if (property !== "") {
this.localValue.push({name: property, value: 0});
this.property = "";
}
},
removeProperty(index) {
this.localValue.splice(index, 1);
},
setValue(index, value) {
if (value.target)
return;
this.localValue[index] = value;
return true;
}
},
mounted() {
this.fetchProperties();
}
}
</script>

View file

@ -0,0 +1,77 @@
<template>
<badge-select-field :value="this.value" :options="this.tags" @addElement="addTag" ref="badgeSelect">
<template v-slot:default="{option, index}">
<span class="badge bg-dark" @click="removeTag(index)">
{{ option }}
</span>
</template>
</badge-select-field>
</template>
<style scoped>
.badge {
padding: .3rem .5rem;
}
.badge:hover {
background-color: var(--bs-danger) !important;
}
</style>
<script>
import * as BIcons from "bootstrap-icons-vue";
import {mapActions, mapState} from "vuex";
import BadgeSelectField from "@/components/BadgeSelectField.vue";
export default {
name: "TagField",
components: {
BadgeSelectField,
...BIcons
},
props: {
value: {
type: Array,
required: true
}
},
model: {
prop: "value",
event: "input"
},
computed: {
...mapState(["tags"]),
availableTags() {
return this.tags.filter(tag => !this.localValue.includes(tag));
},
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit("input", value);
}
}
},
methods: {
...mapActions(["fetchTags"]),
addTag(tag) {
if (this.localValue.includes(tag)) {
this.localValue.splice(this.localValue.indexOf(tag), 1);
this.$refs.badgeSelect.resetSelection();
} else if (this.availableTags.includes(tag)) {
this.localValue.push(tag);
this.$refs.badgeSelect.resetSelection();
}else{
// input does not match any tag
}
},
removeTag(index) {
this.localValue.splice(index, 1);
}
},
mounted() {
this.fetchTags();
}
}
</script>