Skip to content

Commit

Permalink
Merge pull request #505 from madeindjs/WF-37
Browse files Browse the repository at this point in the history
  • Loading branch information
ramedina86 authored Aug 19, 2024
2 parents aba8a6f + ce4082b commit 68167ad
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 49 deletions.
Binary file modified docs/framework/public/components/sliderinput.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
253 changes: 253 additions & 0 deletions src/ui/src/core_components/base/BaseInputRange.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
<template>
<div
role="slider"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="value"
:aria-valuetext="`${value} (${Math.round(progress)}%)`"
class="BaseInputRange"
:class="{
'BaseInputRange--popover-always-visible':
popoverDisplayMode === 'always',
}"
>
<transition>
<div
v-show="isPopoverDisplayed"
class="BaseInputRange__popover"
:style="{ left: popoverLeft }"
>
<span class="BaseInputRange__popover__content">
{{ value }}
</span>
</div>
</transition>
<div
ref="slider"
class="BaseInputRange__slider"
@mousedown="handleMouseDown"
>
<div
class="BaseInputRange__slider__progress"
:style="{ width: `calc(${progress}% + ${thumbRadius}px)` }"
></div>
</div>
<button
ref="thumb"
type="button"
class="BaseInputRange__thumb"
:style="{ left: thumbLeft }"
aria-label="Use the arrow keys to increase or decrease the value."
@keydown.left="updateValue(+model - step)"
@keydown.right="updateValue(+model + step)"
@mousedown="handleMouseDown"
></button>
</div>
</template>

<script setup lang="ts">
import { computed, PropType, ref, toRef, watch } from "vue";
const props = defineProps({
min: { type: Number, default: 0 },
max: { type: Number, default: 100 },
step: { type: Number, default: 1 },
popoverDisplayMode: {
type: String as PropType<"always" | "onChange">,
default: "onChange",
},
});
const model = defineModel("value", { type: Number, default: 50 });
const thumbRadius = 9;
const thumb = ref<HTMLElement>();
const slider = ref<HTMLElement>();
const progress = computed(() => {
if (typeof model.value !== "number") return 50;
return ((model.value - props.min) / (props.max - props.min)) * 100;
});
const isPopoverDisplayed = ref(props.popoverDisplayMode === "always");
let popoverTimeout = null;
watch(toRef(props.popoverDisplayMode), () => {
if (props.popoverDisplayMode === "always") isPopoverDisplayed.value = true;
});
function displayPopover() {
if (props.popoverDisplayMode === "always") return;
isPopoverDisplayed.value = true;
if (popoverTimeout) {
clearTimeout(popoverTimeout);
popoverTimeout = null;
}
popoverTimeout = setTimeout(() => {
isPopoverDisplayed.value = false;
popoverTimeout = null;
}, 1_000);
}
// clamp(0px, calc(62% - 9px), calc(100% - 18px))
const thumbLeft = computed(
() => `clamp(
0px,
calc(${progress.value}% - ${thumbRadius}px),
calc(100% - ${thumbRadius * 2}px)
)`,
);
const popoverLeft = computed(() => `calc(${thumbLeft.value} - 3px)`);
function updateValue(value: number) {
displayPopover();
if (props.min !== undefined && value < props.min) return;
if (props.max !== undefined && value > props.max) return;
// round the value to the closest step
const relativeValue = value - props.min;
const stepIndex = Math.round(relativeValue / props.step);
const roundedValue = props.min + stepIndex * props.step;
if (model.value !== roundedValue) model.value = roundedValue;
}
function handleMouseDown(initialEvent: MouseEvent) {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
const sliderBoundingRect = slider.value.getBoundingClientRect();
// trigger immediate value update to handle user click
onMouseMove(initialEvent);
function onMouseMove(event: MouseEvent) {
const progress =
(event.x - sliderBoundingRect.left) /
(sliderBoundingRect.right - sliderBoundingRect.left);
if (progress > 1 || progress < 0) return;
const value = Math.round(
(props.max - props.min) * progress + props.min,
);
updateValue(value);
}
function onMouseUp() {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
}
}
</script>

<style scoped>
.BaseInputRange {
--thumb-color: var(--accentColor);
--thumb-shadow-color: rgba(228, 231, 237, 0.4);
--slider-color: var(--softenedAccentColor);
--slider-bg-color: var(--separatorColor);
--popover-bg-color: var(--popoverBackgroundColor, rgba(0, 0, 0, 1));
width: 100%;
position: relative;
padding-top: 5px;
padding-bottom: 5px;
}
.BaseInputRange--popover-always-visible {
/* add extra margin to make sure the popover does not overflow on something */
margin-top: 24px;
}
.BaseInputRange__slider {
height: 8px;
width: 100%;
border-radius: 4px;
background-color: var(--slider-bg-color);
}
.BaseInputRange__slider__progress {
height: 100%;
max-width: 100%;
border-radius: 4px;
background-color: var(--slider-color);
}
.BaseInputRange__thumb {
border: none;
height: 18px;
width: 18px;
border-radius: 50%;
background-color: var(--thumb-color);
position: absolute;
top: 0;
transition: box-shadow ease-in-out 0.5s;
}
.BaseInputRange__thumb:active {
box-shadow: 0 0 0 6px var(--thumb-shadow-color);
}
.BaseInputRange__thumb:focus-visible {
box-shadow: 0 0 0 6px var(--thumb-shadow-color);
outline: 1px solid blue;
outline-offset: 2px;
}
.BaseInputRange__popover {
position: absolute;
top: -24px;
font-size: 10px;
z-index: 2;
color: var(--popoverColor, white);
background: var(--popover-bg-color);
height: 18px;
width: 24px;
max-width: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.BaseInputRange__popover::after {
content: "";
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid var(--popover-bg-color);
position: absolute;
bottom: -5px;
left: calc(50% - 4px);
}
.BaseInputRange__popover__content {
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
/* start invisible */
.v-enter-active,
.v-leave-active {
transition: opacity 0.2s ease-in-out;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
5 changes: 4 additions & 1 deletion src/ui/src/core_components/base/BaseJsonViewerValue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { PropType, computed } from "vue";
import type { JsonValue } from "./BaseJsonViewer.vue";
const props = defineProps({
data: { type: Object as PropType<JsonValue>, required: true },
data: {
type: [Object, String, Number] as PropType<JsonValue>,
required: true,
},
});
const dataFormatted = computed(() => JSON.stringify(props.data));
Expand Down
72 changes: 26 additions & 46 deletions src/ui/src/core_components/input/CoreSliderInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,22 @@
:label="fields.label.value"
class="CoreSliderInput"
>
<div class="inputArea">
<input
type="range"
:value="formValue"
:min="fields.minValue.value"
:max="fields.maxValue.value"
:step="fields.stepSize.value"
@input="
($event) =>
handleInput(
($event.target as HTMLInputElement).value,
'wf-number-change',
)
"
/>
<div class="valueContainer">
<h3>{{ formValue }}</h3>
</div>
</div>
<BaseInputRange
popover-display-mode="always"
:value="formValue"
:min="fields.minValue.value"
:max="fields.maxValue.value"
:step="fields.stepSize.value"
@update:value="handleInput($event, 'wf-number-change')"
/>
</BaseInputWrapper>
</template>

<script lang="ts">
import { FieldType } from "../../writerTypes";
import { ComponentPublicInstance } from "vue";
import { accentColor, cssClasses } from "../../renderer/sharedStyleFields";
import { FieldCategory, FieldType } from "../../writerTypes";
import BaseInputWrapper from "../base/BaseInputWrapper.vue";
import { ComponentPublicInstance } from "vue";
const description =
"A user input component that allows users to select numeric values using a slider with optional constraints like min, max, and step.";
Expand Down Expand Up @@ -72,6 +61,20 @@ export default {
init: "1",
},
accentColor,
popoverColor: {
name: "Popover color",
type: FieldType.Color,
category: FieldCategory.Style,
applyStyleVariable: true,
default: "white",
},
popoverBackgroundColor: {
name: "Popover background",
type: FieldType.Color,
category: FieldCategory.Style,
applyStyleVariable: true,
default: "rgba(0, 0, 0, 1)",
},
cssClasses,
},
events: {
Expand All @@ -89,13 +92,14 @@ export default {
import { inject, ref } from "vue";
import injectionKeys from "../../injectionKeys";
import { useFormValueBroker } from "../../renderer/useFormValueBroker";
import BaseInputRange from "../base/BaseInputRange.vue";
const fields = inject(injectionKeys.evaluatedFields);
const rootInstance = ref<ComponentPublicInstance | null>(null);
const wf = inject(injectionKeys.core);
const instancePath = inject(injectionKeys.instancePath);
const { formValue, handleInput } = useFormValueBroker(
const { formValue, handleInput } = useFormValueBroker<number>(
wf,
instancePath,
rootInstance,
Expand All @@ -110,28 +114,4 @@ const { formValue, handleInput } = useFormValueBroker(
width: 100%;
max-width: 50ch;
}
.inputArea {
display: flex;
align-items: center;
gap: 8px;
border-radius: 8px;
border: 1px solid transparent;
}
input {
flex: 1 1 auto;
min-width: 0;
margin: 0;
accent-color: var(--accentColor);
border-radius: 8px;
height: 38px;
outline: none;
}
.valueContainer {
min-width: 0;
flex: 0 0 auto;
text-align: center;
}
</style>
4 changes: 2 additions & 2 deletions src/ui/src/renderer/useFormValueBroker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function useFormValueBroker<T = any>(
() => getBindingValue(),
(value) => {
if (isBusy.value) return;
formValue.value = value;
formValue.value = value as T;
},
{ immediate: true },
);
Expand All @@ -110,7 +110,7 @@ export function useFormValueBroker<T = any>(
formValue,
(newValue) => {
if (typeof newValue === "undefined") {
formValue.value = "";
formValue.value = "" as T;
}
},
{ immediate: true },
Expand Down

0 comments on commit 68167ad

Please sign in to comment.