@tremolo-ui/vue 0.7.0
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/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/index.cjs +2486 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1678 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +1678 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2444 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
- package/src/components/AnimationCanvas/AnimationCanvas.ts +105 -0
- package/src/components/AnimationCanvas/index.ts +5 -0
- package/src/components/DropZone/DropZone.ts +43 -0
- package/src/components/DropZone/index.ts +1 -0
- package/src/components/FileInput/FileInput.ts +82 -0
- package/src/components/FileInput/FileInputTrigger.ts +24 -0
- package/src/components/FileInput/context.ts +18 -0
- package/src/components/FileInput/index.ts +3 -0
- package/src/components/Knob/Knob.ts +237 -0
- package/src/components/Knob/KnobActiveLine.ts +31 -0
- package/src/components/Knob/KnobInactiveLine.ts +36 -0
- package/src/components/Knob/KnobSVGRoot.ts +25 -0
- package/src/components/Knob/KnobThumb.ts +53 -0
- package/src/components/Knob/context.ts +30 -0
- package/src/components/Knob/index.ts +6 -0
- package/src/components/NumberInput/NumberInput.ts +258 -0
- package/src/components/NumberInput/NumberInputDecrementStepper.ts +7 -0
- package/src/components/NumberInput/NumberInputField.ts +128 -0
- package/src/components/NumberInput/NumberInputIncrementStepper.ts +7 -0
- package/src/components/NumberInput/NumberInputStepper.ts +54 -0
- package/src/components/NumberInput/context.ts +63 -0
- package/src/components/NumberInput/index.ts +6 -0
- package/src/components/NumberInput/stepper-button.ts +65 -0
- package/src/components/Piano/Piano.ts +243 -0
- package/src/components/Piano/index.ts +1 -0
- package/src/components/PointsEditor/PointsEditor.ts +146 -0
- package/src/components/PointsEditor/PointsEditorBackground.ts +17 -0
- package/src/components/PointsEditor/PointsEditorContainer.ts +73 -0
- package/src/components/PointsEditor/PointsEditorPoint.ts +253 -0
- package/src/components/PointsEditor/PointsEditorSelectionBox.ts +36 -0
- package/src/components/PointsEditor/context.ts +39 -0
- package/src/components/PointsEditor/index.ts +9 -0
- package/src/components/Slider/Slider.ts +224 -0
- package/src/components/Slider/SliderMarks.ts +57 -0
- package/src/components/Slider/SliderMarksOption.ts +85 -0
- package/src/components/Slider/SliderThumb.ts +92 -0
- package/src/components/Slider/SliderTrack.ts +50 -0
- package/src/components/Slider/context.ts +33 -0
- package/src/components/Slider/index.ts +6 -0
- package/src/components/XYPad/XYPad.ts +250 -0
- package/src/components/XYPad/XYPadArea.ts +20 -0
- package/src/components/XYPad/XYPadThumb.ts +105 -0
- package/src/components/XYPad/context.ts +33 -0
- package/src/components/XYPad/index.ts +4 -0
- package/src/components/_util/context.ts +8 -0
- package/src/components/_util/placement.ts +36 -0
- package/src/components/_util/props.ts +32 -0
- package/src/components/_util/useCheckSteps.ts +22 -0
- package/src/components/_util/visually-hidden-range-input.ts +16 -0
- package/src/composables/useDrag.ts +22 -0
- package/src/composables/useDragValue.ts +20 -0
- package/src/composables/useDropZone.ts +32 -0
- package/src/composables/useInstance.ts +52 -0
- package/src/composables/useLongPress.ts +20 -0
- package/src/composables/useMIDIAccess.ts +58 -0
- package/src/composables/useMIDIInput.ts +33 -0
- package/src/composables/useMIDIMessage.ts +30 -0
- package/src/composables/useWheel.ts +23 -0
- package/src/index.ts +67 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
defineComponent,
|
|
4
|
+
h,
|
|
5
|
+
mergeProps,
|
|
6
|
+
ref,
|
|
7
|
+
useId,
|
|
8
|
+
watch,
|
|
9
|
+
type PropType,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
arrowKeyMove,
|
|
14
|
+
clampPoint,
|
|
15
|
+
createDragValue,
|
|
16
|
+
elementMapping,
|
|
17
|
+
POINT_AXIS,
|
|
18
|
+
selectModifier,
|
|
19
|
+
type InputEventOption,
|
|
20
|
+
type ModifierValue,
|
|
21
|
+
type PointPosition,
|
|
22
|
+
type PointsEditorPoint as PointRegistration,
|
|
23
|
+
} from '@tremolo-ui/dom'
|
|
24
|
+
|
|
25
|
+
import { checkPlacement } from '../_util/placement'
|
|
26
|
+
import { visuallyHiddenRangeInput } from '../_util/visually-hidden-range-input'
|
|
27
|
+
|
|
28
|
+
import { usePointsEditorContext } from './context'
|
|
29
|
+
|
|
30
|
+
type PerAxis = string | Partial<Record<'x' | 'y', string>>
|
|
31
|
+
|
|
32
|
+
const perAxis = (setting: PerAxis | undefined, axis: 'x' | 'y') =>
|
|
33
|
+
typeof setting === 'string' ? setting : setting?.[axis]
|
|
34
|
+
|
|
35
|
+
/** One point, bound with `v-model` as `{ x, y }` from 0 to 1. */
|
|
36
|
+
export const PointsEditorPoint = /* @__PURE__ */ defineComponent({
|
|
37
|
+
name: 'PointsEditorPoint',
|
|
38
|
+
inheritAttrs: false,
|
|
39
|
+
props: {
|
|
40
|
+
/** Where the point is, with `y` growing downwards. Bind it with `v-model`. */
|
|
41
|
+
modelValue: { type: Object as PropType<PointPosition>, required: true },
|
|
42
|
+
/** How the selection refers to this point. One is generated when left out. */
|
|
43
|
+
id: String,
|
|
44
|
+
/** The lowest position the point can take, per axis. */
|
|
45
|
+
min: Object as PropType<Partial<PointPosition>>,
|
|
46
|
+
/** The highest position the point can take, per axis. */
|
|
47
|
+
max: Object as PropType<Partial<PointPosition>>,
|
|
48
|
+
/** Sets `--color`, for the theme to colour the point with. */
|
|
49
|
+
color: String,
|
|
50
|
+
/** Overrides the `disabled` of `PointsEditor`. */
|
|
51
|
+
disabled: { type: Boolean, default: undefined },
|
|
52
|
+
/** Overrides the `readonly` of `PointsEditor`. */
|
|
53
|
+
readonly: { type: Boolean, default: undefined },
|
|
54
|
+
/** Overrides the `wheel` of `PointsEditor`; `null` turns it off. */
|
|
55
|
+
wheel: {
|
|
56
|
+
type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
|
|
57
|
+
default: undefined,
|
|
58
|
+
},
|
|
59
|
+
/** Overrides the `keyboard` of `PointsEditor`; `null` turns it off. */
|
|
60
|
+
keyboard: {
|
|
61
|
+
type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
|
|
62
|
+
default: undefined,
|
|
63
|
+
},
|
|
64
|
+
/** The accessible name of each axis, or one for both. */
|
|
65
|
+
ariaLabel: [String, Object] as PropType<PerAxis>,
|
|
66
|
+
/** What the value of each axis means, or one for both. */
|
|
67
|
+
ariaValuetext: [String, Object] as PropType<PerAxis>,
|
|
68
|
+
},
|
|
69
|
+
emits: {
|
|
70
|
+
'update:modelValue': (value: PointPosition) => typeof value === 'object',
|
|
71
|
+
dragStart: (value: PointPosition) => typeof value === 'object',
|
|
72
|
+
dragEnd: (value: PointPosition) => typeof value === 'object',
|
|
73
|
+
},
|
|
74
|
+
setup(props, { slots, attrs, emit }) {
|
|
75
|
+
const points = usePointsEditorContext()
|
|
76
|
+
checkPlacement('PointsEditorPoint', 'PointsEditorContainer')
|
|
77
|
+
|
|
78
|
+
const generatedId = useId()
|
|
79
|
+
const id = computed(() => props.id ?? generatedId)
|
|
80
|
+
const disabled = computed(() => props.disabled ?? points.disabled)
|
|
81
|
+
const readonly = computed(() => props.readonly ?? points.readonly)
|
|
82
|
+
const inactive = computed(() => disabled.value || readonly.value)
|
|
83
|
+
const wheel = computed(() =>
|
|
84
|
+
props.wheel === undefined ? points.wheel : props.wheel,
|
|
85
|
+
)
|
|
86
|
+
const keyboard = computed(() =>
|
|
87
|
+
props.keyboard === undefined ? points.keyboard : props.keyboard,
|
|
88
|
+
)
|
|
89
|
+
const current = computed(() =>
|
|
90
|
+
clampPoint(props.modelValue, props.min, props.max),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const el = ref<HTMLDivElement | null>(null)
|
|
94
|
+
const x = ref<HTMLInputElement | null>(null)
|
|
95
|
+
const dragging = ref(false)
|
|
96
|
+
|
|
97
|
+
// Read by the editor whenever it moves the selection, so the value can
|
|
98
|
+
// change on every frame of a drag without registering again.
|
|
99
|
+
const read = (): PointRegistration => ({
|
|
100
|
+
value: props.modelValue,
|
|
101
|
+
min: props.min,
|
|
102
|
+
max: props.max,
|
|
103
|
+
readonly: inactive.value,
|
|
104
|
+
onChange: (next) => emit('update:modelValue', next),
|
|
105
|
+
element: el.value,
|
|
106
|
+
wheel: wheel.value,
|
|
107
|
+
})
|
|
108
|
+
watch(
|
|
109
|
+
id,
|
|
110
|
+
(key, _, onCleanup) => onCleanup(points.editor.registerPoint(key, read)),
|
|
111
|
+
{ immediate: true },
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
/** Where the pointer was when the drag started. */
|
|
115
|
+
let origin: PointPosition | null = null
|
|
116
|
+
|
|
117
|
+
watch(
|
|
118
|
+
el,
|
|
119
|
+
(element, _, onCleanup) => {
|
|
120
|
+
if (!element) return
|
|
121
|
+
// The value is the position itself: no scaling, and no rounding.
|
|
122
|
+
const drag = createDragValue(element, {
|
|
123
|
+
axis: POINT_AXIS,
|
|
124
|
+
mapping: elementMapping(() => points.container, {
|
|
125
|
+
sensitivity: (state) =>
|
|
126
|
+
selectModifier(points.dragSensitivity, state.event).value,
|
|
127
|
+
}),
|
|
128
|
+
cursor: inactive.value ? undefined : points.cursor,
|
|
129
|
+
shouldStart: () => !disabled.value,
|
|
130
|
+
// A move rather than a position: the point keeps the offset it was
|
|
131
|
+
// grabbed at, and everything else selected moves with it.
|
|
132
|
+
onChange: ([px, py]) => {
|
|
133
|
+
if (origin) {
|
|
134
|
+
points.editor.movePointDrag({
|
|
135
|
+
x: px - origin.x,
|
|
136
|
+
y: py - origin.y,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
onDragStart: ([px, py], state) => {
|
|
141
|
+
points.editor.beginPointDrag(id.value, state.event)
|
|
142
|
+
origin = { x: px, y: py }
|
|
143
|
+
dragging.value = true
|
|
144
|
+
x.value?.focus()
|
|
145
|
+
if (!inactive.value) emit('dragStart', current.value)
|
|
146
|
+
},
|
|
147
|
+
onDragEnd: () => {
|
|
148
|
+
origin = null
|
|
149
|
+
dragging.value = false
|
|
150
|
+
if (!inactive.value) emit('dragEnd', current.value)
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
const stop = watch(
|
|
154
|
+
() => (inactive.value ? undefined : points.cursor),
|
|
155
|
+
(cursor) => drag.update({ cursor }),
|
|
156
|
+
)
|
|
157
|
+
onCleanup(() => {
|
|
158
|
+
stop()
|
|
159
|
+
drag.destroy()
|
|
160
|
+
})
|
|
161
|
+
},
|
|
162
|
+
{ immediate: true, flush: 'post' },
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
function onInput(axis: 'x' | 'y', target: HTMLInputElement) {
|
|
166
|
+
if (readonly.value) {
|
|
167
|
+
target.value = String(current.value[axis])
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
const delta = target.valueAsNumber - props.modelValue[axis]
|
|
171
|
+
points.editor.nudgeSelection(id.value, {
|
|
172
|
+
x: axis === 'x' ? delta : 0,
|
|
173
|
+
y: axis === 'y' ? delta : 0,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const input = (axis: 'x' | 'y') =>
|
|
178
|
+
visuallyHiddenRangeInput({
|
|
179
|
+
ref: axis === 'x' ? x : undefined,
|
|
180
|
+
'data-axis': axis,
|
|
181
|
+
value: current.value[axis],
|
|
182
|
+
min: props.min?.[axis] ?? 0,
|
|
183
|
+
max: props.max?.[axis] ?? 1,
|
|
184
|
+
step: 'any',
|
|
185
|
+
disabled: disabled.value,
|
|
186
|
+
'aria-readonly': readonly.value,
|
|
187
|
+
'aria-orientation': axis === 'x' ? 'horizontal' : 'vertical',
|
|
188
|
+
'aria-label': perAxis(props.ariaLabel, axis) ?? axis,
|
|
189
|
+
'aria-valuetext': perAxis(props.ariaValuetext, axis),
|
|
190
|
+
onInput: (event: Event) =>
|
|
191
|
+
onInput(axis, event.target as HTMLInputElement),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// The visual point has two values, so its semantics live on the two
|
|
195
|
+
// range inputs inside it. tabindex -1 keeps the focus inside when a press
|
|
196
|
+
// lands on the point itself.
|
|
197
|
+
return () => {
|
|
198
|
+
const { style, onFocus, onKeydown, ...rest } = attrs
|
|
199
|
+
return h(
|
|
200
|
+
'div',
|
|
201
|
+
{
|
|
202
|
+
ref: el,
|
|
203
|
+
tabindex: -1,
|
|
204
|
+
'data-disabled': disabled.value ? '' : undefined,
|
|
205
|
+
'data-readonly': readonly.value ? '' : undefined,
|
|
206
|
+
'data-dragging': dragging.value ? '' : undefined,
|
|
207
|
+
'data-selected': points.selection.includes(id.value) ? '' : undefined,
|
|
208
|
+
...rest,
|
|
209
|
+
style: [
|
|
210
|
+
{
|
|
211
|
+
position: 'absolute',
|
|
212
|
+
translate: 'var(--translate, -50% -50%)',
|
|
213
|
+
'--color': props.color,
|
|
214
|
+
},
|
|
215
|
+
style,
|
|
216
|
+
{
|
|
217
|
+
left: `${props.modelValue.x * 100}%`,
|
|
218
|
+
top: `${props.modelValue.y * 100}%`,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
// Merged with the caller's own, which run after these.
|
|
222
|
+
...mergeProps(
|
|
223
|
+
{
|
|
224
|
+
onFocus: (event: FocusEvent) => {
|
|
225
|
+
// The point is not the control: what reaches it goes to the input.
|
|
226
|
+
if (!disabled.value && event.target === event.currentTarget) {
|
|
227
|
+
x.value?.focus()
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
onKeydown: (event: KeyboardEvent) => {
|
|
231
|
+
// The key picks the axis, whichever input holds the focus. y grows
|
|
232
|
+
// downwards, so ArrowUp moves the point towards 0.
|
|
233
|
+
const move = arrowKeyMove(event.key)
|
|
234
|
+
if (!move) return
|
|
235
|
+
event.preventDefault()
|
|
236
|
+
if (inactive.value || !keyboard.value) return
|
|
237
|
+
points.editor.nudgePoint(
|
|
238
|
+
id.value,
|
|
239
|
+
move.axis === 0 ? 'x' : 'y',
|
|
240
|
+
move.direction,
|
|
241
|
+
keyboard.value,
|
|
242
|
+
event,
|
|
243
|
+
)
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{ onFocus, onKeydown },
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
[input('x'), input('y'), slots.default?.()],
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { checkPlacement } from '../_util/placement'
|
|
4
|
+
|
|
5
|
+
import { usePointsEditorContext } from './context'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The box a drag on empty space draws. Nothing is rendered while no drag is
|
|
9
|
+
* running, and it takes none of the pointer.
|
|
10
|
+
*/
|
|
11
|
+
export const PointsEditorSelectionBox = /* @__PURE__ */ defineComponent({
|
|
12
|
+
name: 'PointsEditorSelectionBox',
|
|
13
|
+
setup(_, { slots }) {
|
|
14
|
+
const points = usePointsEditorContext()
|
|
15
|
+
checkPlacement('PointsEditorSelectionBox', 'PointsEditorContainer')
|
|
16
|
+
return () => {
|
|
17
|
+
const box = points.selectionBox
|
|
18
|
+
if (!box) return null
|
|
19
|
+
return h(
|
|
20
|
+
'div',
|
|
21
|
+
{
|
|
22
|
+
style: {
|
|
23
|
+
position: 'absolute',
|
|
24
|
+
zIndex: 20,
|
|
25
|
+
pointerEvents: 'none',
|
|
26
|
+
left: `${box.x * 100}%`,
|
|
27
|
+
top: `${box.y * 100}%`,
|
|
28
|
+
width: `${box.width * 100}%`,
|
|
29
|
+
height: `${box.height * 100}%`,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
slots.default?.(),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type InputEventOption,
|
|
5
|
+
type ModifierValue,
|
|
6
|
+
type PointsEditorInstance,
|
|
7
|
+
type SelectionBoxRect,
|
|
8
|
+
} from '@tremolo-ui/dom'
|
|
9
|
+
|
|
10
|
+
import { injectContext } from '../_util/context'
|
|
11
|
+
|
|
12
|
+
/** What `PointsEditor` shares with its parts. The fields are getters. */
|
|
13
|
+
export interface PointsEditorContextValue {
|
|
14
|
+
readonly disabled: boolean
|
|
15
|
+
readonly readonly: boolean
|
|
16
|
+
readonly wheel: ModifierValue<InputEventOption> | null
|
|
17
|
+
readonly keyboard: ModifierValue<InputEventOption> | null
|
|
18
|
+
readonly dragSensitivity: ModifierValue<number>
|
|
19
|
+
readonly cursor: string
|
|
20
|
+
readonly selectable: boolean
|
|
21
|
+
/** Ids of the selected points. Always empty while `selectable` is off. */
|
|
22
|
+
readonly selection: readonly string[]
|
|
23
|
+
/** The selection box, while one is being dragged. */
|
|
24
|
+
readonly selectionBox: SelectionBoxRect | null
|
|
25
|
+
/** The element points are placed in and measured against. */
|
|
26
|
+
readonly container: HTMLElement | null
|
|
27
|
+
/** The selection and the moves, from `createPointsEditor` in `@tremolo-ui/dom`. */
|
|
28
|
+
readonly editor: PointsEditorInstance
|
|
29
|
+
/** @internal `PointsEditorContainer` registers its element. */
|
|
30
|
+
setContainer: (element: HTMLElement | null) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const PointsEditorKey: InjectionKey<PointsEditorContextValue> =
|
|
34
|
+
Symbol('PointsEditor')
|
|
35
|
+
|
|
36
|
+
/** The context of the enclosing `PointsEditor`, for a part of your own. */
|
|
37
|
+
export function usePointsEditorContext(): PointsEditorContextValue {
|
|
38
|
+
return injectContext(PointsEditorKey, 'PointsEditor')
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export {
|
|
2
|
+
usePointsEditorContext,
|
|
3
|
+
type PointsEditorContextValue,
|
|
4
|
+
} from './context'
|
|
5
|
+
export { PointsEditor } from './PointsEditor'
|
|
6
|
+
export { PointsEditorBackground } from './PointsEditorBackground'
|
|
7
|
+
export { PointsEditorContainer } from './PointsEditorContainer'
|
|
8
|
+
export { PointsEditorPoint } from './PointsEditorPoint'
|
|
9
|
+
export { PointsEditorSelectionBox } from './PointsEditorSelectionBox'
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { computed, defineComponent, h, provide, ref, type PropType } from 'vue'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyDelta,
|
|
5
|
+
arrowKeyDirection,
|
|
6
|
+
elementMapping,
|
|
7
|
+
selectModifier,
|
|
8
|
+
valuePercent,
|
|
9
|
+
wheelDirection,
|
|
10
|
+
type AxisOptions,
|
|
11
|
+
type XY,
|
|
12
|
+
} from '@tremolo-ui/dom'
|
|
13
|
+
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
14
|
+
|
|
15
|
+
import { useDragValue } from '../../composables/useDragValue'
|
|
16
|
+
import { useWheel } from '../../composables/useWheel'
|
|
17
|
+
import { dragSensitivityProp, keyboardProp, wheelProp } from '../_util/props'
|
|
18
|
+
import { useCheckSteps } from '../_util/useCheckSteps'
|
|
19
|
+
|
|
20
|
+
import { SliderKey } from './context'
|
|
21
|
+
|
|
22
|
+
/** Customizable slider. Bind the value with `v-model`. */
|
|
23
|
+
export const Slider = /* @__PURE__ */ defineComponent({
|
|
24
|
+
name: 'Slider',
|
|
25
|
+
props: {
|
|
26
|
+
/** The current value. Bind it with `v-model`. */
|
|
27
|
+
modelValue: { type: Number, required: true },
|
|
28
|
+
/** The value at the start of the travel. */
|
|
29
|
+
min: { type: Number, required: true },
|
|
30
|
+
/** The value at the end of the travel. */
|
|
31
|
+
max: { type: Number, required: true },
|
|
32
|
+
/** Granularity of the value. @default 1 */
|
|
33
|
+
step: { type: Number, default: 1 },
|
|
34
|
+
/** How the value is distributed across the travel. @default linearScale */
|
|
35
|
+
scale: { type: Object as PropType<Scale>, default: () => linearScale },
|
|
36
|
+
/** Run the slider vertically, with the value growing upwards. */
|
|
37
|
+
vertical: Boolean,
|
|
38
|
+
/** Grow the value the other way. */
|
|
39
|
+
reverse: Boolean,
|
|
40
|
+
/** The cursor to show while dragging. @default { cursor: 'pointer' } */
|
|
41
|
+
externalStyles: Object as PropType<{ cursor?: string }>,
|
|
42
|
+
wheel: wheelProp,
|
|
43
|
+
keyboard: keyboardProp,
|
|
44
|
+
dragSensitivity: dragSensitivityProp,
|
|
45
|
+
/** Make the slider unchangeable and remove it from the tab order. */
|
|
46
|
+
disabled: Boolean,
|
|
47
|
+
/** Make the value unchangeable. */
|
|
48
|
+
readonly: Boolean,
|
|
49
|
+
},
|
|
50
|
+
emits: {
|
|
51
|
+
'update:modelValue': (value: number) => typeof value === 'number',
|
|
52
|
+
/** A drag started, with the value where the track was pressed. */
|
|
53
|
+
dragStart: (value: number) => typeof value === 'number',
|
|
54
|
+
/** The drag ended, with the value it ended on. */
|
|
55
|
+
dragEnd: (value: number) => typeof value === 'number',
|
|
56
|
+
},
|
|
57
|
+
setup(props, { slots, emit, expose }) {
|
|
58
|
+
const root = ref<HTMLDivElement | null>(null)
|
|
59
|
+
let track: HTMLElement | null = null
|
|
60
|
+
let thumb: HTMLInputElement | null = null
|
|
61
|
+
|
|
62
|
+
const inactive = computed(() => props.disabled || props.readonly)
|
|
63
|
+
// Measured from the left or the top, as CSS places things: vertical and
|
|
64
|
+
// reverse each flip it, and together they cancel out.
|
|
65
|
+
const displayReversed = computed(() => props.vertical !== props.reverse)
|
|
66
|
+
const percent = computed(() =>
|
|
67
|
+
valuePercent(
|
|
68
|
+
props.modelValue,
|
|
69
|
+
{ min: props.min, max: props.max, scale: props.scale },
|
|
70
|
+
displayReversed.value,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
const axis = computed((): AxisOptions => ({
|
|
74
|
+
min: props.min,
|
|
75
|
+
max: props.max,
|
|
76
|
+
step: props.step,
|
|
77
|
+
scale: props.scale,
|
|
78
|
+
reverse: displayReversed.value,
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
useCheckSteps(() => ({
|
|
82
|
+
component: 'Slider',
|
|
83
|
+
range: axis.value,
|
|
84
|
+
keyboard: props.keyboard,
|
|
85
|
+
wheel: props.wheel,
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
const change = (next: number) => emit('update:modelValue', next)
|
|
89
|
+
|
|
90
|
+
provide(SliderKey, {
|
|
91
|
+
get value() {
|
|
92
|
+
return props.modelValue
|
|
93
|
+
},
|
|
94
|
+
get min() {
|
|
95
|
+
return props.min
|
|
96
|
+
},
|
|
97
|
+
get max() {
|
|
98
|
+
return props.max
|
|
99
|
+
},
|
|
100
|
+
get step() {
|
|
101
|
+
return props.step
|
|
102
|
+
},
|
|
103
|
+
get scale() {
|
|
104
|
+
return props.scale
|
|
105
|
+
},
|
|
106
|
+
get vertical() {
|
|
107
|
+
return props.vertical
|
|
108
|
+
},
|
|
109
|
+
get reverse() {
|
|
110
|
+
return props.reverse
|
|
111
|
+
},
|
|
112
|
+
get disabled() {
|
|
113
|
+
return props.disabled
|
|
114
|
+
},
|
|
115
|
+
get readonly() {
|
|
116
|
+
return props.readonly
|
|
117
|
+
},
|
|
118
|
+
get percent() {
|
|
119
|
+
return percent.value
|
|
120
|
+
},
|
|
121
|
+
change,
|
|
122
|
+
setTrack: (element) => {
|
|
123
|
+
track = element
|
|
124
|
+
},
|
|
125
|
+
setThumb: (input) => {
|
|
126
|
+
thumb = input
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const valueOf = (v: XY<number>) => v[props.vertical ? 1 : 0]
|
|
131
|
+
|
|
132
|
+
useDragValue(root, () => ({
|
|
133
|
+
axis: axis.value,
|
|
134
|
+
mapping: elementMapping(() => track, {
|
|
135
|
+
sensitivity: (state) =>
|
|
136
|
+
selectModifier(props.dragSensitivity, state.event).value,
|
|
137
|
+
}),
|
|
138
|
+
cursor: inactive.value
|
|
139
|
+
? undefined
|
|
140
|
+
: (props.externalStyles?.cursor ?? 'pointer'),
|
|
141
|
+
shouldStart: () => !inactive.value,
|
|
142
|
+
updateOnPointerDown: true,
|
|
143
|
+
onChange: (v) => {
|
|
144
|
+
if (!inactive.value) change(valueOf(v))
|
|
145
|
+
},
|
|
146
|
+
onDragStart: (v) => {
|
|
147
|
+
if (inactive.value) return
|
|
148
|
+
thumb?.focus()
|
|
149
|
+
emit('dragStart', valueOf(v))
|
|
150
|
+
},
|
|
151
|
+
onDragEnd: (v) => {
|
|
152
|
+
if (!inactive.value) emit('dragEnd', valueOf(v))
|
|
153
|
+
},
|
|
154
|
+
}))
|
|
155
|
+
|
|
156
|
+
useWheel(
|
|
157
|
+
root,
|
|
158
|
+
(event) => {
|
|
159
|
+
if (!props.wheel || inactive.value) return
|
|
160
|
+
// A notch the slider does not read — a sideways scroll on a vertical
|
|
161
|
+
// slider — is left to the page rather than swallowed.
|
|
162
|
+
const direction = wheelDirection(event, { horizontal: !props.vertical })
|
|
163
|
+
if (direction === null) return
|
|
164
|
+
event.preventDefault()
|
|
165
|
+
change(
|
|
166
|
+
applyDelta(
|
|
167
|
+
props.modelValue,
|
|
168
|
+
props.reverse ? -direction : direction,
|
|
169
|
+
props.wheel,
|
|
170
|
+
axis.value,
|
|
171
|
+
event,
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
},
|
|
175
|
+
{ requireFocus: true },
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expose({
|
|
179
|
+
/** Move the focus to the thumb. */
|
|
180
|
+
focus: () => {
|
|
181
|
+
if (!props.disabled) thumb?.focus()
|
|
182
|
+
},
|
|
183
|
+
/** Take the focus away from the thumb. */
|
|
184
|
+
blur: () => thumb?.blur(),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// The group is the pointer and keyboard event area; the range input in
|
|
188
|
+
// the thumb carries the control semantics. A press lands on the track or
|
|
189
|
+
// the thumb, neither of which can hold focus, and the browser answers that
|
|
190
|
+
// by clearing the focus — tabindex -1 keeps it inside.
|
|
191
|
+
return () =>
|
|
192
|
+
h(
|
|
193
|
+
'div',
|
|
194
|
+
{
|
|
195
|
+
ref: root,
|
|
196
|
+
role: 'group',
|
|
197
|
+
tabindex: -1,
|
|
198
|
+
'data-orientation': props.vertical ? 'vertical' : 'horizontal',
|
|
199
|
+
'data-disabled': props.disabled ? '' : undefined,
|
|
200
|
+
'data-readonly': props.readonly ? '' : undefined,
|
|
201
|
+
onKeydown: (event: KeyboardEvent) => {
|
|
202
|
+
const direction = arrowKeyDirection(event.key)
|
|
203
|
+
if (direction === null) return
|
|
204
|
+
event.preventDefault()
|
|
205
|
+
if (!props.keyboard || inactive.value) return
|
|
206
|
+
change(
|
|
207
|
+
applyDelta(
|
|
208
|
+
props.modelValue,
|
|
209
|
+
props.reverse ? -direction : direction,
|
|
210
|
+
props.keyboard,
|
|
211
|
+
axis.value,
|
|
212
|
+
event,
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
},
|
|
216
|
+
onFocus: () => {
|
|
217
|
+
if (!props.disabled) thumb?.focus()
|
|
218
|
+
},
|
|
219
|
+
onBlur: () => thumb?.blur(),
|
|
220
|
+
},
|
|
221
|
+
slots.default?.(),
|
|
222
|
+
)
|
|
223
|
+
},
|
|
224
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { computed, defineComponent, h, type PropType } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { cssLength, sliderMarks, type MarksOptions } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
5
|
+
import { providePlacement } from '../_util/placement'
|
|
6
|
+
|
|
7
|
+
import { useSliderContext } from './context'
|
|
8
|
+
import { SliderMarksOption } from './SliderMarksOption'
|
|
9
|
+
|
|
10
|
+
/** The marks along the slider: written out as options, or built from `options`. */
|
|
11
|
+
export const SliderMarks = /* @__PURE__ */ defineComponent({
|
|
12
|
+
name: 'SliderMarks',
|
|
13
|
+
props: {
|
|
14
|
+
/** Space between the marks and the track. Sets `--gap`. */
|
|
15
|
+
gap: [Number, String],
|
|
16
|
+
/**
|
|
17
|
+
* Build the marks instead of writing `SliderMarksOption` out: a number
|
|
18
|
+
* puts one every that many, `'step'` one every `step`. The default slot
|
|
19
|
+
* is ignored while it is set.
|
|
20
|
+
*/
|
|
21
|
+
options: [String, Number, Object] as PropType<MarksOptions>,
|
|
22
|
+
},
|
|
23
|
+
setup(props, { slots }) {
|
|
24
|
+
const slider = useSliderContext()
|
|
25
|
+
providePlacement('SliderMarks')
|
|
26
|
+
const marks = computed(() => {
|
|
27
|
+
if (props.options === undefined) return []
|
|
28
|
+
const list = sliderMarks(
|
|
29
|
+
props.options,
|
|
30
|
+
slider.min,
|
|
31
|
+
slider.max,
|
|
32
|
+
slider.step,
|
|
33
|
+
)
|
|
34
|
+
// In the order they are on screen.
|
|
35
|
+
return slider.vertical !== slider.reverse ? list.reverse() : list
|
|
36
|
+
})
|
|
37
|
+
// Each option inside is placed against this box.
|
|
38
|
+
return () =>
|
|
39
|
+
h(
|
|
40
|
+
'div',
|
|
41
|
+
{
|
|
42
|
+
'data-orientation': slider.vertical ? 'vertical' : 'horizontal',
|
|
43
|
+
style: { '--gap': cssLength(props.gap), position: 'relative' },
|
|
44
|
+
},
|
|
45
|
+
props.options !== undefined
|
|
46
|
+
? marks.value.map(({ value, mark, label }) =>
|
|
47
|
+
h(SliderMarksOption, {
|
|
48
|
+
key: value,
|
|
49
|
+
value,
|
|
50
|
+
mark,
|
|
51
|
+
label: label ? undefined : null,
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
: slots.default?.(),
|
|
55
|
+
)
|
|
56
|
+
},
|
|
57
|
+
})
|