dashboard/components/form/ArrayList.vue

322 lines
6.8 KiB
Vue

<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@/config/query-params';
import { removeAt } from '@/utils/array';
import TextAreaAutoGrow from '@/components/form/TextAreaAutoGrow';
import { clone } from '@/utils/object';
const DEFAULT_PROTIP = 'Tip: Paste lines into any list field for easy bulk entry';
export default {
components: { TextAreaAutoGrow },
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
initialEmptyRow: {
type: Boolean,
default: false,
},
title: {
type: String,
default: ''
},
protip: {
type: [String, Boolean],
default: DEFAULT_PROTIP,
},
showHeader: {
type: Boolean,
default: false,
},
valueLabel: {
type: String,
default: 'Value',
},
valuePlaceholder: {
type: String,
default: 'e.g. bar'
},
valueMultiline: {
type: Boolean,
default: false,
},
addLabel: {
type: String,
default() {
return this.$store.getters['i18n/t']('generic.add');
},
},
addAllowed: {
type: Boolean,
default: true,
},
removeLabel: {
type: String,
default() {
return this.$store.getters['i18n/t']('generic.remove');
},
},
removeAllowed: {
type: Boolean,
default: true,
},
defaultAddValue: {
type: [String, Number, Object, Array],
default: ''
},
},
data() {
const input = (this.value || []).slice();
const rows = [];
for ( const value of input ) {
rows.push({ value });
}
if ( !rows.length && this.initialEmptyRow ) {
const value = this.defaultAddValue ? clone(this.defaultAddValue) : '';
rows.push({ value });
}
return { rows, lastUpdateWasFromValue: false };
},
computed: {
isView() {
return this.mode === _VIEW;
},
showAdd() {
return this.addAllowed;
},
showRemove() {
return this.removeAllowed;
},
isDefaultProtip() {
return this.protip === DEFAULT_PROTIP;
},
showProtip() {
if (this.protip && !this.isDefaultProtip) {
return true;
}
return !this.valueMultiline && this.protip;
}
},
watch: {
value() {
this.lastUpdateWasFromValue = true;
this.rows = (this.value || []).map(v => ({ value: v }));
},
rows: {
deep: true,
handler(newValue, oldValue) {
// lastUpdateWasFromValue is used to break a cycle where when rows are updated
// this was called which then forced rows to updated again
if (!this.lastUpdateWasFromValue) {
this.queueUpdate();
}
this.lastUpdateWasFromValue = false;
}
}
},
created() {
this.queueUpdate = debounce(this.update, 50);
},
methods: {
add() {
this.rows.push({ value: clone(this.defaultAddValue) });
if (this.defaultAddValue) {
this.queueUpdate();
}
this.$nextTick(() => {
const inputs = this.$refs.value;
if ( inputs && inputs.length > 0 ) {
inputs[inputs.length - 1].focus();
}
this.$emit('add');
});
},
remove(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if ( this.isView ) {
return;
}
const out = [];
for ( const row of this.rows ) {
const trim = !this.valueMultiline && (typeof row.value === 'string');
const value = trim ? row.value.trim() : row.value;
if ( typeof value !== 'undefined' ) {
out.push(value);
}
}
this.$emit('input', out);
},
onPaste(index, event) {
if (this.valueMultiline) {
return;
}
event.preventDefault();
const text = event.clipboardData.getData('text/plain');
const split = text.split('\n').map(value => ({ value }));
this.rows.splice(index, 1, ...split);
this.update();
}
},
};
</script>
<template>
<div>
<div v-if="title" class="clearfix">
<slot name="title">
<h3>
{{ title }}
<i v-if="showProtip" v-tooltip="protip" class="icon icon-info" />
</h3>
</slot>
</div>
<template v-if="rows.length">
<div v-if="showHeader">
<slot name="column-headers">
<label class="value text-label mb-10">
{{ valueLabel }}
</label>
</slot>
</div>
<div
v-for="(row, idx) in rows"
:key="idx"
class="box"
>
<slot
name="columns"
:queueUpdate="queueUpdate"
:i="idx"
:rows="rows"
:row="row"
:mode="mode"
:isView="isView"
>
<div class="value">
<slot
name="value"
:row="row"
:mode="mode"
:isView="isView"
:queue-update="queueUpdate"
>
<TextAreaAutoGrow
v-if="valueMultiline"
ref="value"
v-model="row.value"
:placeholder="valuePlaceholder"
:mode="mode"
@paste="onPaste(idx, $event)"
@input="queueUpdate"
/>
<input
v-else
ref="value"
v-model="row.value"
:placeholder="valuePlaceholder"
:disabled="isView"
@paste="onPaste(idx, $event)"
@input="queueUpdate"
/>
</slot>
</div>
</slot>
<div v-if="showRemove" class="remove">
<slot name="remove-button" :remove="() => remove(idx)" i="idx">
<button type="button" :disabled="isView" class="btn role-link" @click="remove(idx)">
{{ removeLabel }}
</button>
</slot>
</div>
</div>
</template>
<div v-else-if="mode==='view'" class="text-muted">
&mdash;
</div>
<div v-else>
<slot name="empty" />
</div>
<div v-if="showAdd" class="footer">
<slot v-if="showAdd" name="add">
<button type="button" :disabled="isView" class="btn role-tertiary add" @click="add()">
{{ addLabel }}
</button>
</slot>
</div>
</div>
</template>
<style lang="scss" scoped>
.title {
margin-bottom: 10px;
}
.box {
display: grid;
grid-template-columns: auto $array-list-remove-margin;
align-items: center;
margin-bottom: 10px;
.value {
flex: 1;
INPUT {
height: $input-height;
}
}
}
.remove {
text-align: right;
}
.footer {
.protip {
float: right;
padding: 5px 0;
}
}
</style>