@energie360/ui-library 0.1.22 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/index.js +1 -0
- package/components/slider/slider.scss +248 -0
- package/components/slider/u-slider.vue +163 -0
- package/components/tooltip/tooltip.scss +4 -2
- package/components/tooltip/u-tooltip.vue +20 -2
- package/elements/index.js +1 -0
- package/elements/range-slider/u-range-slider.vue +138 -0
- package/modules/dialog/u-dialog.vue +2 -2
- package/package.json +1 -1
- package/utils/math/scale-value.js +13 -0
package/components/index.js
CHANGED
|
@@ -62,3 +62,4 @@ export { default as USkeletonLoader } from './skeleton-loader/u-skeleton-loader.
|
|
|
62
62
|
export { default as UCardHighlight } from './card-highlight/u-card-highlight.vue'
|
|
63
63
|
export { default as URating } from './rating/u-rating.vue'
|
|
64
64
|
export { default as UChip } from './chip/u-chip.vue'
|
|
65
|
+
export { default as USlider } from './slider/u-slider.vue'
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
@use '../../base/abstracts' as a;
|
|
2
|
+
|
|
3
|
+
@mixin thumb-reset {
|
|
4
|
+
appearance: none;
|
|
5
|
+
border: none;
|
|
6
|
+
height: var(--slider-thumb-height);
|
|
7
|
+
width: var(--slider-thumb-width);
|
|
8
|
+
border-radius: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.slider {
|
|
12
|
+
--slider-height: 60px;
|
|
13
|
+
--slider-width: 100%;
|
|
14
|
+
--slider-track-height: 4px;
|
|
15
|
+
--slider-upper-fill-color: rgb(150 150 150);
|
|
16
|
+
--slider-lower-fill-color: rgb(0 150 0);
|
|
17
|
+
--slider-thumb-height: 60px;
|
|
18
|
+
--slider-thumb-width: 60px;
|
|
19
|
+
|
|
20
|
+
position: relative;
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
column-gap: var(--e-space-4);
|
|
24
|
+
width: var(--slider-width);
|
|
25
|
+
height: var(--slider-height);
|
|
26
|
+
|
|
27
|
+
// Style native input
|
|
28
|
+
input::-moz-range-thumb {
|
|
29
|
+
@include thumb-reset;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
input::-webkit-slider-thumb {
|
|
33
|
+
@include thumb-reset;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// States
|
|
37
|
+
&.is-sliding {
|
|
38
|
+
.slider__dot,
|
|
39
|
+
.slider__dot-wrapper {
|
|
40
|
+
pointer-events: none;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&.is-sliding.is-touch {
|
|
45
|
+
.slider__thumb {
|
|
46
|
+
transform: translateX(-50%) translateY(-50%);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.slider__min-value,
|
|
52
|
+
.slider__max-value {
|
|
53
|
+
@include a.type(200);
|
|
54
|
+
|
|
55
|
+
flex: 0 0 auto;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.slider__slider {
|
|
59
|
+
flex: 1 0 auto;
|
|
60
|
+
position: relative;
|
|
61
|
+
height: 100%;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.slider__input {
|
|
65
|
+
position: absolute;
|
|
66
|
+
top: 0;
|
|
67
|
+
left: calc(var(--slider-thumb-width) / -2);
|
|
68
|
+
right: calc(var(--slider-thumb-width) / -2);
|
|
69
|
+
height: 100%;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
|
|
72
|
+
input {
|
|
73
|
+
width: 100%;
|
|
74
|
+
height: 100%;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.slider__control {
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 100%;
|
|
81
|
+
touch-action: none;
|
|
82
|
+
|
|
83
|
+
&::before {
|
|
84
|
+
content: '';
|
|
85
|
+
position: absolute;
|
|
86
|
+
top: calc(50% - var(--slider-track-height) / 2);
|
|
87
|
+
height: var(--slider-track-height);
|
|
88
|
+
width: 100%;
|
|
89
|
+
background-color: var(--e-c-primary-01-100);
|
|
90
|
+
border-radius: var(--e-brd-radius-1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.slider__dot {
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
border-radius: 100%;
|
|
101
|
+
background-color: var(--e-c-mono-00);
|
|
102
|
+
border: 2px solid var(--e-c-primary-01-500);
|
|
103
|
+
pointer-events: auto;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
|
|
106
|
+
&::before {
|
|
107
|
+
content: '';
|
|
108
|
+
position: absolute;
|
|
109
|
+
display: block;
|
|
110
|
+
width: a.rem(8);
|
|
111
|
+
height: a.rem(8);
|
|
112
|
+
border-radius: 100%;
|
|
113
|
+
background-color: var(--e-c-primary-01-500);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&:hover {
|
|
117
|
+
&::before {
|
|
118
|
+
background-color: var(--e-c-primary-01-700);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
&:active {
|
|
123
|
+
border-color: var(--e-c-primary-01-700);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Dot variants
|
|
127
|
+
&.upper {
|
|
128
|
+
border-color: var(--e-c-mono-200);
|
|
129
|
+
|
|
130
|
+
&::before {
|
|
131
|
+
width: a.rem(4);
|
|
132
|
+
height: a.rem(4);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&:hover {
|
|
136
|
+
&::before {
|
|
137
|
+
width: a.rem(8);
|
|
138
|
+
height: a.rem(8);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
&:active {
|
|
143
|
+
border-color: var(--e-c-primary-01-700);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
&.highlight {
|
|
148
|
+
border-color: var(--e-c-mono-700);
|
|
149
|
+
|
|
150
|
+
&::before {
|
|
151
|
+
background-color: var(--e-c-mono-700);
|
|
152
|
+
width: a.rem(8);
|
|
153
|
+
height: a.rem(8);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
&::after {
|
|
157
|
+
content: '';
|
|
158
|
+
position: absolute;
|
|
159
|
+
display: block;
|
|
160
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='9' fill='none'%3E%3Cpath fill='%236B6B6B' d='M6.79 7.985a1 1 0 0 1-1.58 0L.256 1.614A1 1 0 0 1 1.045 0h9.91a1 1 0 0 1 .79 1.614L6.789 7.985Z'/%3E%3C/svg%3E");
|
|
161
|
+
width: 12px;
|
|
162
|
+
height: 9px;
|
|
163
|
+
left: 2px;
|
|
164
|
+
top: -12px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&:active,
|
|
168
|
+
&:hover {
|
|
169
|
+
border-color: var(--e-c-primary-01-500);
|
|
170
|
+
|
|
171
|
+
&::before {
|
|
172
|
+
background-color: var(--e-c-primary-01-500);
|
|
173
|
+
width: a.rem(8);
|
|
174
|
+
height: a.rem(8);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
&::after {
|
|
178
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='9' fill='none'%3E%3Cpath fill='%234BA528' d='M6.79 7.985a1 1 0 0 1-1.58 0L.256 1.614A1 1 0 0 1 1.045 0h9.91a1 1 0 0 1 .79 1.614L6.789 7.985Z'/%3E%3C/svg%3E");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.slider__dot-wrapper {
|
|
185
|
+
position: absolute;
|
|
186
|
+
display: block;
|
|
187
|
+
top: -6px;
|
|
188
|
+
width: a.rem(16);
|
|
189
|
+
height: a.rem(16);
|
|
190
|
+
transform: translateX(-50%);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.slider__tooltip-placeholder {
|
|
194
|
+
display: block;
|
|
195
|
+
width: 12px;
|
|
196
|
+
height: 12px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.slider__thumb {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
position: relative;
|
|
204
|
+
height: var(--slider-thumb-height);
|
|
205
|
+
width: var(--slider-thumb-width);
|
|
206
|
+
background-color: rgba(var(--e-c-primary-01-200-rgb), 0.5);
|
|
207
|
+
border-radius: 100%;
|
|
208
|
+
top: calc(var(--slider-thumb-height) / -2 + (var(--slider-track-height) / 2));
|
|
209
|
+
transform: translateX(-50%);
|
|
210
|
+
transition: transform var(--e-trs-duration-fastest) var(--e-trs-easing-default);
|
|
211
|
+
|
|
212
|
+
&::before {
|
|
213
|
+
content: '';
|
|
214
|
+
position: absolute;
|
|
215
|
+
background-color: var(--e-c-primary-01-700);
|
|
216
|
+
inset: 6px;
|
|
217
|
+
border-radius: 100%;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.slider__thumb-value {
|
|
222
|
+
@include a.type(200, strong);
|
|
223
|
+
|
|
224
|
+
position: relative;
|
|
225
|
+
white-space: nowrap;
|
|
226
|
+
color: var(--e-c-mono-00);
|
|
227
|
+
user-select: none;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.slider__track {
|
|
231
|
+
top: calc(50% - var(--slider-track-height) / 2);
|
|
232
|
+
position: relative;
|
|
233
|
+
height: var(--slider-track-height);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.slider__lower-fill {
|
|
237
|
+
position: absolute;
|
|
238
|
+
top: 0;
|
|
239
|
+
left: 0;
|
|
240
|
+
height: 4px;
|
|
241
|
+
border-top-left-radius: var(--e-brd-radius-1);
|
|
242
|
+
border-bottom-left-radius: var(--e-brd-radius-1);
|
|
243
|
+
background-color: var(--e-c-primary-01-500);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.visually-hidden {
|
|
247
|
+
@include a.visually-hidden;
|
|
248
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { watch, useId, useTemplateRef, onMounted, ref } from 'vue'
|
|
3
|
+
import { scaleValue } from '../../utils/math/scale-value'
|
|
4
|
+
import { clamp } from '../../utils/math/clamp'
|
|
5
|
+
import { UTooltip } from '../'
|
|
6
|
+
|
|
7
|
+
interface SliderDot {
|
|
8
|
+
value: number
|
|
9
|
+
isHighlighted?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// TODO: I get an error with importing interface RangeSlider and extending it here. No idea why this happens...
|
|
13
|
+
interface Props {
|
|
14
|
+
name: string
|
|
15
|
+
dots?: SliderDot[]
|
|
16
|
+
label: string
|
|
17
|
+
min?: number
|
|
18
|
+
max?: number
|
|
19
|
+
step?: number
|
|
20
|
+
valuePrefix?: string
|
|
21
|
+
valueSuffix?: string
|
|
22
|
+
hideMinMax?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
dots = [],
|
|
27
|
+
min = 0,
|
|
28
|
+
max = 100,
|
|
29
|
+
step = 1,
|
|
30
|
+
valuePrefix = '',
|
|
31
|
+
valueSuffix = '',
|
|
32
|
+
} = defineProps<Props>()
|
|
33
|
+
|
|
34
|
+
const model = defineModel<number>()
|
|
35
|
+
const emits = defineEmits(['change'])
|
|
36
|
+
const id = useId()
|
|
37
|
+
const isSliding = ref(false)
|
|
38
|
+
const isTouch = ref(false)
|
|
39
|
+
const thumbEl = useTemplateRef('thumb')
|
|
40
|
+
const lowerFillEl = useTemplateRef('lower-fill')
|
|
41
|
+
const controlEl = useTemplateRef('control')
|
|
42
|
+
|
|
43
|
+
const getValueString = (v) => `${valuePrefix}${v}${valueSuffix}`
|
|
44
|
+
|
|
45
|
+
const getLeftPosition = (v) => scaleValue(Number(v), Number(min), Number(max), 0, 100, 2)
|
|
46
|
+
const setThumbPosition = () => {
|
|
47
|
+
const position = getLeftPosition(model.value)
|
|
48
|
+
|
|
49
|
+
thumbEl.value.style.left = `${position}%`
|
|
50
|
+
lowerFillEl.value.style.width = `${position}%`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const setModelValueFromLeftPosition = (leftPos) => {
|
|
54
|
+
model.value = scaleValue(leftPos, 0, 100, min, max, 0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMounted(() => {
|
|
58
|
+
setThumbPosition()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
watch(model, () => {
|
|
62
|
+
setThumbPosition()
|
|
63
|
+
emits('change', model.value)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const onPointerdown = (e: PointerEvent) => {
|
|
67
|
+
if (e.target.closest('.slider__dot')) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
isSliding.value = true
|
|
72
|
+
|
|
73
|
+
// Get correct left position
|
|
74
|
+
const leftPos = Math.round(
|
|
75
|
+
((e.clientX - controlEl.value.getBoundingClientRect().left) / controlEl.value.offsetWidth) *
|
|
76
|
+
100,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
isTouch.value = e.pointerType !== 'mouse'
|
|
80
|
+
|
|
81
|
+
setModelValueFromLeftPosition(leftPos)
|
|
82
|
+
controlEl.value.addEventListener('pointermove', onPointermove)
|
|
83
|
+
document.addEventListener('pointerup', onPointerup)
|
|
84
|
+
}
|
|
85
|
+
const onPointermove = (e: PointerEvent) => {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
|
|
88
|
+
// Get correct left position
|
|
89
|
+
let leftPos = Math.round(
|
|
90
|
+
((e.clientX - controlEl.value.getBoundingClientRect().left) / controlEl.value.offsetWidth) *
|
|
91
|
+
100,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
leftPos = clamp(leftPos, 0, 100)
|
|
95
|
+
|
|
96
|
+
setModelValueFromLeftPosition(leftPos)
|
|
97
|
+
}
|
|
98
|
+
const onPointerup = () => {
|
|
99
|
+
isSliding.value = false
|
|
100
|
+
|
|
101
|
+
controlEl.value.removeEventListener('pointermove', onPointermove)
|
|
102
|
+
document.removeEventListener('pointerup', onPointerup)
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<div :class="['slider', { 'is-sliding': isSliding, 'is-touch': isTouch }]">
|
|
108
|
+
<label class="visually-hidden" :for="id">{{ label }}</label>
|
|
109
|
+
|
|
110
|
+
<div v-if="!hideMinMax" class="slider__min-value">
|
|
111
|
+
{{ getValueString(min) }}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="slider__slider">
|
|
115
|
+
<div class="slider__input">
|
|
116
|
+
<input :id v-model="model" :name type="range" :min :max :step @change="setThumbPosition" />
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div ref="control" class="slider__control" @pointerdown.prevent="onPointerdown">
|
|
120
|
+
<div class="slider__track">
|
|
121
|
+
<div ref="lower-fill" class="slider__lower-fill"></div>
|
|
122
|
+
|
|
123
|
+
<div
|
|
124
|
+
v-for="(dot, idx) in dots"
|
|
125
|
+
:key="idx"
|
|
126
|
+
class="slider__dot-wrapper"
|
|
127
|
+
:style="{ left: `${getLeftPosition(dot.value)}%` }"
|
|
128
|
+
>
|
|
129
|
+
<UTooltip
|
|
130
|
+
:title="getValueString(dot.value)"
|
|
131
|
+
style="--tooltip-min-width: 0"
|
|
132
|
+
:disabled="isSliding"
|
|
133
|
+
:offset="10"
|
|
134
|
+
:offset-x="1"
|
|
135
|
+
>
|
|
136
|
+
<button
|
|
137
|
+
:class="[
|
|
138
|
+
'slider__dot',
|
|
139
|
+
`${dot.value < model ? 'lower' : 'upper'}`,
|
|
140
|
+
{ highlight: dot.isHighlighted },
|
|
141
|
+
]"
|
|
142
|
+
type="button"
|
|
143
|
+
@click="model = dot.value"
|
|
144
|
+
>
|
|
145
|
+
<span class="visually-hidden">{{ getValueString(dot.value) }}</span>
|
|
146
|
+
</button>
|
|
147
|
+
</UTooltip>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<button ref="thumb" type="button" class="slider__thumb" tabindex="-1">
|
|
151
|
+
<span class="slider__thumb-value">{{ getValueString(model) }} </span>
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div v-if="!hideMinMax" class="slider__max-value">
|
|
158
|
+
{{ getValueString(max) }}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</template>
|
|
162
|
+
|
|
163
|
+
<style scoped lang="scss" src="./slider.scss"></style>
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
@use '../../base/abstracts
|
|
1
|
+
@use '../../base/abstracts' as a;
|
|
2
2
|
|
|
3
3
|
.tooltip {
|
|
4
|
+
--tooltip-min-width: 84px;
|
|
5
|
+
|
|
4
6
|
pointer-events: none;
|
|
5
7
|
z-index: a.$layer-tooltip;
|
|
6
8
|
position: absolute;
|
|
7
9
|
top: 0;
|
|
8
10
|
left: 0;
|
|
9
11
|
max-width: a.rem(360);
|
|
10
|
-
min-width:
|
|
12
|
+
min-width: var(--tooltip-min-width);
|
|
11
13
|
opacity: 0;
|
|
12
14
|
transform: translateY(-5px);
|
|
13
15
|
transition:
|
|
@@ -5,9 +5,18 @@ import type { PopoverPositionParams } from './popover'
|
|
|
5
5
|
|
|
6
6
|
interface Props extends PopoverPositionParams {
|
|
7
7
|
title: string
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
offsetX?: number
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
placement = 'top-center',
|
|
14
|
+
offset = 12,
|
|
15
|
+
viewportPadding = 20,
|
|
16
|
+
disabled = false,
|
|
17
|
+
// This optional prop is to potentially fix some slightly off calculations.
|
|
18
|
+
offsetX = 0,
|
|
19
|
+
} = defineProps<Props>()
|
|
11
20
|
|
|
12
21
|
const tooltip = ref<HTMLDivElement | null>(null)
|
|
13
22
|
const root = ref<HTMLSpanElement | null>(null)
|
|
@@ -18,10 +27,18 @@ const isTooltipVisible = ref<boolean>(false)
|
|
|
18
27
|
// For mobile and touch devices it's th opposite, pointer events will be fired first.
|
|
19
28
|
// This way we can detect on what device we are and handle the behaviour accordingly.
|
|
20
29
|
function hideTooltip() {
|
|
30
|
+
if (disabled) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
isTooltipVisible.value = false
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
function showTooltip() {
|
|
38
|
+
if (disabled) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
updatePositions()
|
|
26
43
|
isTooltipVisible.value = true
|
|
27
44
|
}
|
|
@@ -40,7 +57,7 @@ function updatePositions(resetPositions = false) {
|
|
|
40
57
|
})
|
|
41
58
|
|
|
42
59
|
tooltip.value!.style.top = resetPositions ? '' : `${top}px`
|
|
43
|
-
tooltip.value!.style.left = resetPositions ? '' : `${left}px`
|
|
60
|
+
tooltip.value!.style.left = resetPositions ? '' : `${left + offsetX}px`
|
|
44
61
|
// Fix pointer position if necessary
|
|
45
62
|
pointer.value!.style.transform =
|
|
46
63
|
!resetPositions && leftCorrection ? `translateX(${leftCorrection * -1}px)` : ''
|
|
@@ -57,6 +74,7 @@ function updatePositions(resetPositions = false) {
|
|
|
57
74
|
ref="tooltip"
|
|
58
75
|
:class="['tooltip', { 'tooltip--open': isTooltipVisible }]"
|
|
59
76
|
:aria-hidden="!isTooltipVisible"
|
|
77
|
+
v-bind="$attrs"
|
|
60
78
|
@transitionend="onTransitionend"
|
|
61
79
|
>
|
|
62
80
|
<div class="tooltip__content">
|
package/elements/index.js
CHANGED
|
@@ -22,3 +22,4 @@ export { default as UToggleSwitch } from './toggle-switch/u-toggle-switch.vue'
|
|
|
22
22
|
export { default as UCheckbox } from './checkbox/u-checkbox.vue'
|
|
23
23
|
export { default as USelectTile } from './select-tile/u-select-tile.vue'
|
|
24
24
|
export { default as USelectTiles } from './select-tiles/u-select-tiles.vue'
|
|
25
|
+
// export { default as URangeSlider } from './range-slider/u-range-slider.vue'
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// NOTE: URangeSlider as a base widget was a good idea.
|
|
3
|
+
// But at the moment not really usable.
|
|
4
|
+
// Maybe deprecate it.
|
|
5
|
+
|
|
6
|
+
import { useTemplateRef, onMounted, useId, watch } from 'vue'
|
|
7
|
+
import { scaleValue } from '../../utils/math/scale-value'
|
|
8
|
+
|
|
9
|
+
export interface RangeSlider {
|
|
10
|
+
label: string
|
|
11
|
+
min?: number
|
|
12
|
+
max?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { min = 0, max = 100 } = defineProps<RangeSlider>()
|
|
16
|
+
|
|
17
|
+
const id = useId()
|
|
18
|
+
const model = defineModel<number>()
|
|
19
|
+
const thumbEl = useTemplateRef('thumb')
|
|
20
|
+
const lowerFillEl = useTemplateRef('lower-fill')
|
|
21
|
+
let thumbWidth
|
|
22
|
+
|
|
23
|
+
const setThumbPosition = () => {
|
|
24
|
+
const position = scaleValue(Number(model.value), Number(min), Number(max), 0, 100, 2)
|
|
25
|
+
thumbEl.value.style.left = `${position}%`
|
|
26
|
+
|
|
27
|
+
lowerFillEl.value.style.width = `calc(${position}% + ${thumbWidth / 2}px)`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onMounted(() => {
|
|
31
|
+
// Store thumb width. We'll judt assume that this won't change during runtime.
|
|
32
|
+
thumbWidth = thumbEl.value.getBoundingClientRect().width
|
|
33
|
+
|
|
34
|
+
setThumbPosition()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
watch(model, () => {
|
|
38
|
+
setThumbPosition()
|
|
39
|
+
})
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div class="range-slider">
|
|
44
|
+
<label class="visually-hidden" :for="id">{{ label }}</label>
|
|
45
|
+
<div class="range-slider__input">
|
|
46
|
+
<input
|
|
47
|
+
:id
|
|
48
|
+
v-model="model"
|
|
49
|
+
type="range"
|
|
50
|
+
:min
|
|
51
|
+
:max
|
|
52
|
+
@input="setThumbPosition"
|
|
53
|
+
@change="setThumbPosition"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="range-slider__slider">
|
|
57
|
+
<div class="range-slider__track">
|
|
58
|
+
<div ref="lower-fill" class="range-slider__lower-fill"></div>
|
|
59
|
+
<div ref="thumb" class="range-slider__thumb"></div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<style lang="scss" scoped>
|
|
66
|
+
@use '../../base/abstracts' as a;
|
|
67
|
+
|
|
68
|
+
.range-slider {
|
|
69
|
+
--range-height: 24px;
|
|
70
|
+
--range-width: 100%;
|
|
71
|
+
--range-border-radius: 0;
|
|
72
|
+
--range-upper-fill-color: rgba(150, 150, 150);
|
|
73
|
+
--range-lower-fill-color: rgb(0, 150, 0);
|
|
74
|
+
--range-thumb-height: 24px;
|
|
75
|
+
--range-thumb-width: 12px;
|
|
76
|
+
--range-thumb-border-radius: 0;
|
|
77
|
+
--range-thumb-background-color: rgb(0, 0, 0);
|
|
78
|
+
--range-thumb-transform: none;
|
|
79
|
+
--range-thumb-transition: none;
|
|
80
|
+
|
|
81
|
+
/* "display: contents" would be better, but it still has some accessiblity issues -> https://ericwbailey.website/published/display-contents-considered-harmful/ */
|
|
82
|
+
position: relative;
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
width: var(--range-width);
|
|
85
|
+
height: var(--range-height);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.range-slider__input {
|
|
89
|
+
display: flex;
|
|
90
|
+
width: 100%;
|
|
91
|
+
|
|
92
|
+
input {
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.range-slider__slider {
|
|
98
|
+
position: absolute;
|
|
99
|
+
top: 0;
|
|
100
|
+
left: 0;
|
|
101
|
+
width: 100%;
|
|
102
|
+
height: 100%;
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
background-color: var(--range-upper-fill-color);
|
|
105
|
+
border-radius: var(--range-border-radius);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.range-slider__thumb {
|
|
109
|
+
position: relative;
|
|
110
|
+
height: var(--range-thumb-height);
|
|
111
|
+
width: var(--range-thumb-width);
|
|
112
|
+
background-color: var(--range-thumb-background-color);
|
|
113
|
+
border-radius: var(--range-thumb-border-radius);
|
|
114
|
+
transform: var(--range-thumb-transform);
|
|
115
|
+
transition: var(--range-thumb-transition);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.range-slider__track {
|
|
119
|
+
position: relative;
|
|
120
|
+
width: calc(100% - var(--range-thumb-width));
|
|
121
|
+
height: 100%;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.range-slider__lower-fill {
|
|
125
|
+
position: absolute;
|
|
126
|
+
top: 0;
|
|
127
|
+
left: 0;
|
|
128
|
+
height: 100%;
|
|
129
|
+
border-top-left-radius: var(--range-border-radius);
|
|
130
|
+
border-bottom-left-radius: var(--range-border-radius);
|
|
131
|
+
background-color: var(--range-lower-fill-color);
|
|
132
|
+
min-width: var(--range-thumb-width);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.visually-hidden {
|
|
136
|
+
@include a.visually-hidden;
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
@@ -88,7 +88,7 @@ watch(visible, (newV) => {
|
|
|
88
88
|
'dialog-container',
|
|
89
89
|
mobileDialogStyle,
|
|
90
90
|
{
|
|
91
|
-
'has-header-image': !!slots['header-image'] || headerImage,
|
|
91
|
+
'has-header-image': !!slots['header-image'] || (headerImage?.src && headerImage?.alt),
|
|
92
92
|
'has-content-image': !!slots['content-image'] || contentImage,
|
|
93
93
|
},
|
|
94
94
|
]"
|
|
@@ -128,7 +128,7 @@ watch(visible, (newV) => {
|
|
|
128
128
|
|
|
129
129
|
<div class="cta-container">
|
|
130
130
|
<slot name="cta"></slot>
|
|
131
|
-
<UButton @click="visible = false">{{ closeBtnLabel }}</UButton>
|
|
131
|
+
<UButton @click="visible = false">{{ closeBtnLabel || getTranslation('close') }}</UButton>
|
|
132
132
|
</div>
|
|
133
133
|
</div>
|
|
134
134
|
</dialog>
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const scaleValue = (
|
|
2
|
+
srcValue,
|
|
3
|
+
srcRangeMin,
|
|
4
|
+
srcRangeMax,
|
|
5
|
+
targetRangeMin,
|
|
6
|
+
targetRangeMax,
|
|
7
|
+
round = 0,
|
|
8
|
+
) => {
|
|
9
|
+
const dec = Math.pow(10, round)
|
|
10
|
+
const scale = (targetRangeMax - targetRangeMin) / (srcRangeMax - srcRangeMin)
|
|
11
|
+
|
|
12
|
+
return Math.round((targetRangeMin + (srcValue - srcRangeMin) * scale) * dec) / dec
|
|
13
|
+
}
|