diff --git a/docs/framework/public/components/rangeinput.png b/docs/framework/public/components/rangeinput.png new file mode 100644 index 000000000..22d790efb Binary files /dev/null and b/docs/framework/public/components/rangeinput.png differ diff --git a/src/ui/src/components/core/base/BaseInputSlider.utils.ts b/src/ui/src/components/core/base/BaseInputSlider.utils.ts new file mode 100644 index 000000000..1394d6a8a --- /dev/null +++ b/src/ui/src/components/core/base/BaseInputSlider.utils.ts @@ -0,0 +1,14 @@ +import { computed, Ref } from "vue"; + +/** + * Format a number using `toFixed` according to the number of floating number in the `step` + */ +export function useNumberFormatByStep( + value: Ref, + step: Ref, +) { + const precision = computed( + () => String(step.value).split(".")[1]?.length ?? 0, + ); + return computed(() => Number(value.value).toFixed(precision.value)); +} diff --git a/src/ui/src/components/core/base/BaseInputSlider.vue b/src/ui/src/components/core/base/BaseInputSlider.vue new file mode 100644 index 000000000..bd6d16d29 --- /dev/null +++ b/src/ui/src/components/core/base/BaseInputSlider.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/ui/src/components/core/base/BaseInputSliderLayout.vue b/src/ui/src/components/core/base/BaseInputSliderLayout.vue new file mode 100644 index 000000000..3069a7979 --- /dev/null +++ b/src/ui/src/components/core/base/BaseInputSliderLayout.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/ui/src/components/core/base/BaseInputSliderRange.vue b/src/ui/src/components/core/base/BaseInputSliderRange.vue new file mode 100644 index 000000000..738787c95 --- /dev/null +++ b/src/ui/src/components/core/base/BaseInputSliderRange.vue @@ -0,0 +1,120 @@ + + + diff --git a/src/ui/src/components/core/base/BaseInputRange.vue b/src/ui/src/components/core/base/BaseInputSliderThumb.vue similarity index 63% rename from src/ui/src/components/core/base/BaseInputRange.vue rename to src/ui/src/components/core/base/BaseInputSliderThumb.vue index c23db79e6..f63ee1e59 100644 --- a/src/ui/src/components/core/base/BaseInputRange.vue +++ b/src/ui/src/components/core/base/BaseInputSliderThumb.vue @@ -1,75 +1,52 @@ - - diff --git a/src/ui/src/composables/useBoundingClientRect.ts b/src/ui/src/composables/useBoundingClientRect.ts new file mode 100644 index 000000000..f688f7aa1 --- /dev/null +++ b/src/ui/src/composables/useBoundingClientRect.ts @@ -0,0 +1,18 @@ +import { Ref, ref, onUnmounted, computed } from "vue"; + +/** + * Watch the bounding client rect of an element using `setInterval` + */ +export function useBoundingClientRect(htmlRef: Ref, ms = 500) { + const rect = ref(); + + const id = setInterval(() => { + rect.value = htmlRef.value?.getBoundingClientRect(); + }, ms); + + onUnmounted(() => { + clearInterval(id); + }); + + return computed(() => rect.value); +} diff --git a/src/ui/src/core/templateMap.ts b/src/ui/src/core/templateMap.ts index 22c3d281f..b05dd014a 100644 --- a/src/ui/src/core/templateMap.ts +++ b/src/ui/src/core/templateMap.ts @@ -28,6 +28,7 @@ import CoreNumberInput from "../components/core/input/CoreNumberInput.vue"; import CoreRadioInput from "../components/core/input/CoreRadioInput.vue"; import CoreSelectInput from "../components/core/input/CoreSelectInput.vue"; import CoreSliderInput from "../components/core/input/CoreSliderInput.vue"; +import CoreSliderRangeInput from "../components/core/input/CoreSliderRangeInput.vue"; import CoreTextInput from "../components/core/input/CoreTextInput.vue"; import CoreTextareaInput from "../components/core/input/CoreTextareaInput.vue"; import CoreTimeInput from "../components/core/input/CoreTimeInput.vue"; @@ -102,6 +103,7 @@ const templateMap = { textareainput: CoreTextareaInput, numberinput: CoreNumberInput, sliderinput: CoreSliderInput, + rangeinput: CoreSliderRangeInput, colorinput: CoreColorInput, dateinput: CoreDateInput, timeinput: CoreTimeInput, diff --git a/src/writer/core.py b/src/writer/core.py index 0fa7d5668..1387771f2 100644 --- a/src/writer/core.py +++ b/src/writer/core.py @@ -11,6 +11,7 @@ import logging import math import multiprocessing +import numbers import re import secrets import time @@ -1388,6 +1389,26 @@ def _transform_time_change(self, ev) -> str: return payload + def _transform_range_change(self, ev) -> list[int]: + payload = ev.payload + + if not isinstance(payload, list): + raise ValueError("Range must be an array.") + + if len(payload) != 2: + raise ValueError("Range must contains exactly two values.") + + if not isinstance(payload[0], numbers.Real): + raise ValueError("First item is not a number.") + + if not isinstance(payload[1], numbers.Real): + raise ValueError("Second item is not a number.") + + if payload[0] > payload[1]: + raise ValueError("First item is higher than second.") + + return payload + def _transform_change_page_size(self, ev) -> Optional[int]: try: return int(ev.payload) diff --git a/tests/backend/test_core.py b/tests/backend/test_core.py index bbbf262eb..da5221d13 100644 --- a/tests/backend/test_core.py +++ b/tests/backend/test_core.py @@ -875,6 +875,23 @@ def test_date_change(self) -> None: self.ed.transform(ev_valid) assert ev_valid.payload == "2019-11-23" + def test_range_change(self) -> None: + ev_invalid = WriterEvent( + type="wf-range-change", + instancePath=self.root_instance_path, + payload="virus" + ) + with pytest.raises(RuntimeError): + self.ed.transform(ev_invalid) + + ev_valid = WriterEvent( + type="wf-range-change", + instancePath=self.root_instance_path, + payload=[10,42] + ) + self.ed.transform(ev_valid) + assert ev_valid.payload == [10, 42] + def test_time_change(self) -> None: ev_invalid = WriterEvent( type="wf-time-change", diff --git a/tests/e2e/tests/components.spec.ts b/tests/e2e/tests/components.spec.ts index 8abfea5cd..f4f315e5d 100644 --- a/tests/e2e/tests/components.spec.ts +++ b/tests/e2e/tests/components.spec.ts @@ -29,6 +29,7 @@ const mapComponents = { dateinput: {locator: '.component.wf-type-dateinput label'}, timeinput: {locator: '.component.wf-type-timeinput label'}, sliderinput: {locator: '.component.wf-type-sliderinput label'}, + rangeinput: {locator: '.component.wf-type-rangeinput label'}, numberinput: {locator: '.component.wf-type-numberinput label'}, textinput: {locator: '.component.wf-type-textinput label'}, }