Skip to content

Commit

Permalink
Allow sliders to be locked/unlocked (#495)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrishamm committed Dec 11, 2024
1 parent 0d67a32 commit 402adf9
Showing 1 changed file with 135 additions and 119 deletions.
254 changes: 135 additions & 119 deletions src/components/inputs/PercentageInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<template>
<v-row dense align="center">
<v-col cols="auto">
<v-btn v-if="!numericInputs && canLock" large icon :color="isLocked ? 'error' : undefined" :disabled="disabled" class="me-1"
@click="isLocked = !isLocked">
<v-icon>{{ isLocked ? "mdi-lock" : "mdi-lock-off" }}</v-icon>
</v-btn>

<v-btn large icon :disabled="disabled || innerValue <= min" @click="applyStep(-step)"
@mousedown="mouseDown(false)" @mouseup="mouseUp(false)" @mouseleave="mouseUp(false)"
@touchstart="mouseDown(false)" @touchend="mouseUp(false)" class="ml-0">
Expand All @@ -15,9 +20,10 @@
</v-col>

<v-col v-if="numericInputs" class="d-flex align-center">
<v-combobox ref="input" type="number" :min="min" :max="max" step="any" :disabled="disabled" class="mx-2 mt-2"
append-outer-icon="mdi-percent" :items="items" hide-selected :menu-props="{ maxHeight: '50%' }"
:value="innerValue" @update:search-input="updateValue" @keyup.enter="apply">
<v-combobox ref="input" type="number" :min="min" :max="max" step="any" :disabled="disabled"
class="mx-2 mt-2" append-outer-icon="mdi-percent" :items="items" hide-selected
:menu-props="{ maxHeight: '50%' }" :value="innerValue" @update:search-input="updateValue"
@keyup.enter="apply">
</v-combobox>
<v-btn class="mr-1" color="primary" :disabled="!canApply" @click="apply">
<v-icon class="mr-2">mdi-check</v-icon>
Expand All @@ -26,7 +32,7 @@
</v-col>
<v-col v-else>
<v-slider :value="innerValue" @change="$emit('input', $event)" :min="min" :max="max" :disabled="disabled"
hide-details thumb-label="always" class="slider"></v-slider>
:readonly="isLocked && canLock" hide-details thumb-label="always" class="slider" />
</v-col>

<v-col cols="auto">
Expand All @@ -39,12 +45,27 @@
</v-row>
</template>

<script lang="ts">
import Vue from "vue";
<script setup lang="ts">
import { computed, getCurrentInstance, ref, watch } from "vue";
import store from "@/store";
import { isNumber } from "@/utils/numbers";
const props = withDefaults(defineProps<{
value: number,
min?: number,
max?: number,
step?: number,
disabled?: boolean
}>(), {
min: 0,
max: 100,
step: 5,
disabled: false
});
const emit = defineEmits(["input"]);
/**
* Time needed before the slider value is actually applied (in ms)
*/
Expand All @@ -55,125 +76,120 @@ const debounceTime = 500;
*/
const changeTime = 300, changeInterval = 150;
export default Vue.extend({
props: {
value: {
type: Number,
required: true
},
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
},
step: {
type: Number,
default: 5
},
disabled: Boolean
},
computed: {
numericInputs(): boolean { return store.state.settings.numericInputs; },
canApply(): boolean {
if (this.disabled || this.innerValue === Math.round(this.value) || this.debounceTimer || this.decreaseTimer || this.increaseTimer) {
return false;
}
return isNumber(this.innerValue) && this.innerValue >= this.min && this.innerValue <= this.max;
},
items(): Array<number> {
if (store.state.settings.disableAutoComplete || !this.step) {
return [];
}
// General input logic
const result = [];
if (isNumber(this.min) && isNumber(this.max)) {
for (let value = this.min; value <= this.max; value += this.step) {
result.push(value);
}
} else {
for (let i = 5; i >= 1; i--) {
const lowerValue = this.value - this.step * i;
if (lowerValue >= this.min) {
result.push(lowerValue);
}
}
for (let i = 1; i <= 5; i++) {
const higherValue = this.value + this.step * i;
if (higherValue > this.max) {
break;
}
result.push(higherValue);
}
}
return result;
}
},
data() {
return {
innerValue: this.value,
debounceTimer: null as NodeJS.Timeout | null,
decreaseTimer: null as NodeJS.Timeout | null,
increaseTimer: null as NodeJS.Timeout | null
const innerValue = ref(props.value), input = ref<HTMLInputElement | null>(null);
const canApply = computed(() => {
if (props.disabled || innerValue.value === Math.round(props.value) || debounceTimer || decreaseTimer || increaseTimer) {
return false;
}
return isNumber(innerValue) && innerValue.value >= props.min && innerValue.value <= props.max;
});
function apply() {
(input as any).isMenuActive = false; // FIXME There must be a better solution than this
if (canApply.value) {
emit("input", innerValue.value);
}
}
function updateValue(value: string) {
innerValue.value = parseFloat(value);
}
watch(() => props.value, (to) => {
const newValue = Math.round(to);
if (innerValue.value !== newValue) {
innerValue.value = newValue;
}
});
// Numeric input logic
const numericInputs = computed(() => store.state.settings.numericInputs);
const items = computed(() => {
if (store.state.settings.disableAutoComplete || !props.step) {
return [];
}
const result = [];
if (isNumber(props.min) && isNumber(props.max)) {
for (let value = props.min; value <= props.max; value += props.step) {
result.push(value);
}
},
methods: {
apply() {
(this.$refs.input as any).isMenuActive = false; // FIXME There must be a better solution than this
if (this.canApply) {
this.$emit("input", this.innerValue);
} else {
for (let i = 5; i >= 1; i--) {
const lowerValue = props.value - props.step * i;
if (lowerValue >= props.min) {
result.push(lowerValue);
}
},
updateValue(value: string) {
this.innerValue = parseFloat(value);
},
applyStep(diff: number) {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.innerValue = Math.round(Math.min(this.max, Math.max(this.min, this.innerValue + diff)));
this.debounceTimer = setTimeout(this.debounce, debounceTime);
},
debounce() {
this.$emit("input", this.innerValue);
this.debounceTimer = null;
},
mouseDown(increment: boolean) {
if (increment) {
this.increaseTimer = setTimeout(this.increase, changeTime);
} else {
this.decreaseTimer = setTimeout(this.decrease, changeTime);
}
},
mouseUp(increment: boolean) {
if (increment && this.increaseTimer !== null) {
clearTimeout(this.increaseTimer);
this.increaseTimer = null;
} else if (this.decreaseTimer !== null) {
clearTimeout(this.decreaseTimer);
this.decreaseTimer = null;
}
},
decrease() {
this.applyStep(-this.step);
this.decreaseTimer = setTimeout(this.decrease, changeInterval);
},
increase() {
this.applyStep(this.step);
this.increaseTimer = setTimeout(this.increase, changeInterval);
}
},
watch: {
value(to: number) {
const newValue = Math.round(to);
if (this.innerValue !== newValue) {
this.innerValue = newValue;
for (let i = 1; i <= 5; i++) {
const higherValue = props.value + props.step * i;
if (higherValue > props.max) {
break;
}
result.push(higherValue);
}
}
return result;
});
// Slider logic
const useVuetify = () => {
const vm = getCurrentInstance();
return vm?.proxy?.$vuetify || undefined;
};
const canLock = computed(() => useVuetify()?.breakpoint.mobile);
const isLocked = ref(true);
// Continuous increment/decrement
const debounceTimer = ref<NodeJS.Timeout | null>(null), decreaseTimer = ref<NodeJS.Timeout | null>(null), increaseTimer = ref<NodeJS.Timeout | null>(null);
function applyStep(diff: number) {
if (debounceTimer.value) {
clearTimeout(debounceTimer.value);
}
innerValue.value = Math.round(Math.min(props.max, Math.max(props.min, innerValue.value + diff)));
debounceTimer.value = setTimeout(debounce, debounceTime);
}
function debounce() {
emit("input", innerValue.value);
debounceTimer.value = null;
}
function mouseDown(increment: boolean) {
if (increment) {
increaseTimer.value = setTimeout(increase, changeTime);
} else {
decreaseTimer.value = setTimeout(decrease, changeTime);
}
}
function mouseUp(increment: boolean) {
if (increment && increaseTimer.value !== null) {
clearTimeout(increaseTimer.value);
increaseTimer.value = null;
} else if (decreaseTimer.value !== null) {
clearTimeout(decreaseTimer.value);
decreaseTimer.value = null;
}
}
function decrease() {
applyStep(-props.step);
decreaseTimer.value = setTimeout(decrease, changeInterval);
}
function increase() {
applyStep(props.step);
increaseTimer.value = setTimeout(increase, changeInterval);
}
</script>

0 comments on commit 402adf9

Please sign in to comment.