166 lines
4.5 KiB
JavaScript
166 lines
4.5 KiB
JavaScript
/*
|
|
* Mixin to handle inputs that update the database.
|
|
* Won't bash the field's value while it's focused, even if the database trims
|
|
* or otherwise sanitizes the data captured.
|
|
*
|
|
* Emits a change event that requires acknowledgement with an optional error
|
|
* message if something went wrong
|
|
*/
|
|
import { debounce } from 'lodash';
|
|
|
|
export default {
|
|
inject: {
|
|
context: { default: {} }
|
|
},
|
|
inheritAttrs: false,
|
|
data(){ return {
|
|
error: false,
|
|
ackErrors: null,
|
|
rulesErrors: null,
|
|
focused: false,
|
|
loading: false,
|
|
dirty: false,
|
|
safeValue: this.value,
|
|
inputValue: this.value,
|
|
};},
|
|
props: {
|
|
value: [String, Number, Date, Array, Object, Boolean],
|
|
errorMessages: [String, Array],
|
|
disabled: Boolean,
|
|
debounce: {
|
|
type: Number,
|
|
default: undefined,
|
|
},
|
|
rules: Array,
|
|
},
|
|
watch: {
|
|
focused(newFocus){
|
|
// If the value updated while we were focused, show it now on defocus
|
|
// but not if we are waiting for our own writes to get persisted
|
|
// and not if there is an error in our input
|
|
if (!newFocus && !this.dirty && !this.error){
|
|
this.forceSafeValueUpdate();
|
|
}
|
|
// Start the loading bar on defocus if the input is dirty
|
|
// It might be a lie, we aren't doing the work yet, but it feels laggy
|
|
// to defocus an element and then it starts working after a delay
|
|
if (
|
|
!newFocus &&
|
|
this.dirty &&
|
|
!(this.rulesErrors && this.rulesErrors.length)
|
|
){
|
|
if (this.hasChangeListener) this.loading = true;
|
|
}
|
|
},
|
|
dirty(newDirty){
|
|
// Our changes were acknowledged, weren't in error, and we aren't focused,
|
|
// make sure the internal value matches the database value
|
|
if (!newDirty && !this.focused && !this.error){
|
|
this.forceSafeValueUpdate();
|
|
}
|
|
},
|
|
value(newValue){
|
|
if (
|
|
!this.focused &&
|
|
!(this.rulesErrors && this.rulesErrors.length)
|
|
){
|
|
this.safeValue = newValue;
|
|
}
|
|
},
|
|
safeValue(){
|
|
// The safe value only gets updated from the parent, so it must be valid
|
|
this.error = false;
|
|
this.ackErrors = null;
|
|
},
|
|
},
|
|
methods: {
|
|
input(val){
|
|
this.$emit('input', val);
|
|
this.inputValue = val;
|
|
this.dirty = true;
|
|
|
|
// Apply the rules if there are any
|
|
this.rulesErrors = null;
|
|
if (this.rules && this.rules.length){
|
|
this.rules.forEach(rule => {
|
|
const result = rule(val);
|
|
if (typeof result === 'string'){
|
|
if (!this.rulesErrors) this.rulesErrors = [];
|
|
this.rulesErrors.push(result);
|
|
}
|
|
});
|
|
}
|
|
if (this.rulesErrors){
|
|
return;
|
|
}
|
|
|
|
this.debouncedChange(val);
|
|
},
|
|
acknowledgeChange(error){
|
|
this.loading = false;
|
|
this.dirty = false;
|
|
this.error = !!error;
|
|
if (!error){
|
|
this.ackErrors = null;
|
|
} else if (typeof error === 'string'){
|
|
this.ackErrors = error;
|
|
} else if (error.reason){
|
|
this.ackErrors = error.reason;
|
|
} else if (error.message){
|
|
this.ackErrors = error.message;
|
|
} else {
|
|
this.ackErrors = 'Something went wrong'
|
|
console.error(error);
|
|
}
|
|
},
|
|
change(val){
|
|
this.dirty = true;
|
|
if (this.hasChangeListener()) this.loading = true;
|
|
this.$emit('change', val, this.acknowledgeChange);
|
|
},
|
|
hasChangeListener(){
|
|
return this.$listeners && this.$listeners.change;
|
|
},
|
|
forceSafeValueUpdate(){
|
|
// hack to force the value to update on the child component
|
|
this.safeValue = null;
|
|
this.$nextTick(() => this.safeValue = this.value);
|
|
},
|
|
focus(){
|
|
this.$refs.input.focus();
|
|
}
|
|
},
|
|
computed: {
|
|
errors(){
|
|
let errors = this.ackErrors ? [this.ackErrors] : [];
|
|
if (Array.isArray(this.rulesErrors)){
|
|
errors.push(...this.rulesErrors)
|
|
}
|
|
if (Array.isArray(this.errorMessages)){
|
|
errors.push(...this.errorMessages);
|
|
} else if (typeof this.errorMessages === 'string' && this.errorMessages){
|
|
errors.push(this.errorMessages);
|
|
}
|
|
return errors;
|
|
},
|
|
isDisabled(){
|
|
return this.context.editPermission === false || this.disabled;
|
|
},
|
|
debounceTime() {
|
|
if (Number.isFinite(this.debounce)){
|
|
return this.debounce;
|
|
} else if (Number.isFinite(this.context.debounceTime)){
|
|
return this.context.debounceTime;
|
|
} else {
|
|
return 750;
|
|
}
|
|
},
|
|
},
|
|
created(){
|
|
this.debouncedChange = debounce(this.change, this.debounceTime);
|
|
},
|
|
beforeDestroy(){
|
|
this.debouncedChange.flush();
|
|
},
|
|
};
|