Colour propery box on input, flash green on update, only modal notify if value is not set exactly

This commit is contained in:
Julian Stirling 2025-08-28 14:44:34 +01:00
parent 80ee303ac9
commit 212b8fe062
3 changed files with 90 additions and 9 deletions

View file

@ -6,6 +6,7 @@
<input
v-model="internalValue"
class="uk-form-small numeric-setting-line-input"
:class="{ edited: isEdited, flash: animateUpdate }"
type="number"
@focusin="focusIn"
@focusout="focusOut"
@ -35,7 +36,9 @@
:key="i"
v-model="internalValue[i - 1]"
class="uk-form-small numeric-setting-line-input"
:class="{ edited: isEdited, flash: animateUpdate }"
type="number"
@input="updateIsEdited"
@focusin="focusIn"
@focusout="focusOut"
@keydown="keyDown"
@ -51,7 +54,9 @@
<input
v-model="internalValue[key]"
class="uk-form-small numeric-setting-line-input"
:class="{ edited: isEdited, flash: animateUpdate }"
type="number"
@input="updateIsEdited"
@focusin="focusIn"
@focusout="focusOut"
@keydown="keyDown"
@ -95,19 +100,50 @@ export default {
type: String,
default: ""
},
animate: {
type: Boolean,
default: false
}
},
data() {
return {
internalValue: this.value,
valueOnEnter: undefined,
focused: false
// Initialise with a copy. As this is faster than stringifying and parsing.
internalValue: Array.isArray(this.value)
? [...this.value]
: typeof this.value === 'object'
? { ...this.value }
: this.value,
// Is edited can't be computed as we mutate internalValue
isEdited: false,
animateUpdate: false,
};
},
mounted() {
if (this.value !== undefined) {
this.resetInternalValue();
}
},
watch: {
value(newValue) {
this.internalValue = newValue;
value() {
// Fire updateIsEdited on both value and internal value change,
// as change in value may not causse internalValue to change.
this.updateIsEdited();
this.resetInternalValue();
},
internalValue() {
this.updateIsEdited();
},
animate(updated) {
if (updated) {
this.animateUpdate = true;
setTimeout(() => {
this.animateUpdate = false;
this.$emit('animationShown');
}, 700);
}
}
},
computed: {
@ -170,6 +206,10 @@ export default {
},
methods: {
resetInternalValue: function() {
// stringify and parse to ensure no internal mutation for objects
this.internalValue = JSON.parse(JSON.stringify(this.value));
},
requestUpdate: async function() {
this.$emit("requestUpdate")
},
@ -195,7 +235,23 @@ export default {
if (event.keyCode == 13) {
this.sendValue();
}
},
updateIsEdited: function() {
this.isEdited = this.deepStringify(this.internalValue) !== this.deepStringify(this.value);
},
deepStringify: function(val) {
if (Array.isArray(val)) {
return JSON.stringify(val.map(String));
}
if (val && typeof val === 'object') {
const normalized = Object.fromEntries(
Object.entries(val).map(([k, v]) => [k, String(v)])
);
return JSON.stringify(normalized);
}
return JSON.stringify(String(val));
}
}
};
</script>
@ -215,4 +271,14 @@ export default {
margin-right: 5px;
width: 6em;
}
.edited {
background-color: #fff3cd;
}
@keyframes green-flash {
0% { background-color: #3fda63; }
100% { background-color: white; }
}
.flash {
animation: green-flash 0.7s ease;
}
</style>

View file

@ -3,8 +3,10 @@
v-model="value"
:data-schema="propertyDescription"
:label="label"
:animate="animate"
@requestUpdate="readProperty"
@sendValue="writeProperty"
@animationShown="resetAnimate"
/>
</template>
@ -45,7 +47,8 @@ export default {
data() {
return {
value: undefined
value: undefined,
animate: false
};
},
@ -98,14 +101,15 @@ export default {
await new Promise(r => setTimeout(r, this.readBackDelay));
let newVal = await this.readProperty();
if (newVal == requestedValue) {
await this.modalNotify(`Set ${this.label} to ${newVal}.`);
this.animate = true;
} else {
this.animate = true;
await this.modalNotify(
`Set ${this.label} to ${newVal} (requested ${requestedValue}).`
`Set ${this.label} to ${newVal} (closest valid value to requested ${requestedValue}).`
);
}
} else {
await this.modalNotify(`Set ${this.label} to ${this.value}.`);
this.animate = true;
}
} catch (error) {
// Use mixin to display error
@ -113,6 +117,9 @@ export default {
// Re-read property to try to update to server value
this.readProperty();
}
},
resetAnimate: function() {
this.animate = false;
}
}
};

View file

@ -10,8 +10,10 @@
v-model="backgroundDetectorStatus.settings"
:data-schema="backgroundDetectorStatus.settings_schema"
label=""
:animate="animate"
@requestUpdate="readSettings"
@sendValue="writeSettings"
@animationShown="resetAnimate"
/>
</div>
</li>
@ -57,6 +59,7 @@ export default {
data() {
return {
backgroundDetectorStatus: undefined,
animate: false
};
},
@ -87,6 +90,11 @@ export default {
"update_detector_settings",
{"data": requestedValue}
);
this.animate = true;
this.readSettings();
},
resetAnimate: function() {
this.animate = false;
}
},
async created() {