@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,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
defineComponent,
|
|
4
|
+
h,
|
|
5
|
+
ref,
|
|
6
|
+
watch,
|
|
7
|
+
type HTMLAttributes,
|
|
8
|
+
type PropType,
|
|
9
|
+
type SlotsType,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
blackKeyWidth,
|
|
14
|
+
createPianoInput,
|
|
15
|
+
fitWhiteKeyWidth,
|
|
16
|
+
getNoteRangeArray,
|
|
17
|
+
notePosition,
|
|
18
|
+
pianoWidth,
|
|
19
|
+
type KeyboardShortcuts,
|
|
20
|
+
type KeyboardShortcutsScope,
|
|
21
|
+
type NoteRange,
|
|
22
|
+
type PianoInputInstance,
|
|
23
|
+
type PianoLayout,
|
|
24
|
+
} from '@tremolo-ui/dom'
|
|
25
|
+
import { isWhiteKey, noteKey } from '@tremolo-ui/functions'
|
|
26
|
+
|
|
27
|
+
/** What a key is, when the `label` slot or `keyProps` is asked about it. */
|
|
28
|
+
export interface KeyState {
|
|
29
|
+
/** Position in the note range, counting from `noteRange.first`. */
|
|
30
|
+
index: number
|
|
31
|
+
keyType: 'white' | 'black'
|
|
32
|
+
/** Whether the note is currently sounding. */
|
|
33
|
+
active: boolean
|
|
34
|
+
/** Whether the note is above `midiMax` and cannot sound. */
|
|
35
|
+
disabled: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What `keyProps` may return for one key. */
|
|
39
|
+
export type KeyAttributes = HTMLAttributes &
|
|
40
|
+
Record<`data-${string}`, string | number | boolean | undefined>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Customizable piano. The keys are drawn by the component; what goes inside
|
|
44
|
+
* one is the `label` slot, which receives the note and its state.
|
|
45
|
+
*/
|
|
46
|
+
export const Piano = /* @__PURE__ */ defineComponent({
|
|
47
|
+
name: 'Piano',
|
|
48
|
+
props: {
|
|
49
|
+
/** The notes drawn, from `first` to `last`. */
|
|
50
|
+
noteRange: { type: Object as PropType<NoteRange>, required: true },
|
|
51
|
+
/** Let a pointer slide from one key to the next. @default true */
|
|
52
|
+
glissando: { type: Boolean, default: true },
|
|
53
|
+
/** Highest note that can sound. @default 127 */
|
|
54
|
+
midiMax: { type: Number, default: 127 },
|
|
55
|
+
/** Play notes from the computer keyboard. See `SHORTCUTS` in `@tremolo-ui/dom`. */
|
|
56
|
+
keyboardShortcuts: Object as PropType<KeyboardShortcuts>,
|
|
57
|
+
/** Where keyboard shortcuts listen. @default 'root' */
|
|
58
|
+
keyboardShortcutsScope: {
|
|
59
|
+
type: String as PropType<KeyboardShortcutsScope>,
|
|
60
|
+
default: 'root',
|
|
61
|
+
},
|
|
62
|
+
/** Follow the width of the parent element. */
|
|
63
|
+
resizable: Boolean,
|
|
64
|
+
/** Width of a white key in pixels. @default 40 */
|
|
65
|
+
whiteKeyWidth: { type: Number, default: 40 },
|
|
66
|
+
/** Space between two white keys, in pixels. @default 1 */
|
|
67
|
+
keyGap: { type: Number, default: 1 },
|
|
68
|
+
/** Width of a black key, as a fraction of a white one. @default 0.65 */
|
|
69
|
+
blackKeyWidthRatio: { type: Number, default: 0.65 },
|
|
70
|
+
/** Height of a black key, as a fraction of the keyboard. @default 0.6 */
|
|
71
|
+
blackKeyHeightRatio: { type: Number, default: 0.6 },
|
|
72
|
+
/** Classes for the label inside each key. */
|
|
73
|
+
classes: Object as PropType<{
|
|
74
|
+
keyLabelWrapper?: string
|
|
75
|
+
keyLabel?: string
|
|
76
|
+
}>,
|
|
77
|
+
/**
|
|
78
|
+
* Extra attributes for one key, by note. The geometry of the key is
|
|
79
|
+
* applied after the returned style and cannot be overridden.
|
|
80
|
+
*/
|
|
81
|
+
keyProps: Function as PropType<
|
|
82
|
+
(note: number, state: KeyState) => KeyAttributes
|
|
83
|
+
>,
|
|
84
|
+
},
|
|
85
|
+
emits: {
|
|
86
|
+
/** A note started sounding. */
|
|
87
|
+
playNote: (note: number, _velocity?: number) => typeof note === 'number',
|
|
88
|
+
/** Everything holding a note let go of it. */
|
|
89
|
+
stopNote: (note: number) => typeof note === 'number',
|
|
90
|
+
},
|
|
91
|
+
slots: Object as SlotsType<{
|
|
92
|
+
label: { note: number; state: KeyState }
|
|
93
|
+
}>,
|
|
94
|
+
setup(props, { slots, emit, expose }) {
|
|
95
|
+
const root = ref<HTMLDivElement | null>(null)
|
|
96
|
+
const activeNotes = ref<number[]>([])
|
|
97
|
+
/** Set while `resizable` is on, measured from the parent. */
|
|
98
|
+
const resizedKeyWidth = ref<number | null>(null)
|
|
99
|
+
|
|
100
|
+
const notes = computed(() => getNoteRangeArray(props.noteRange))
|
|
101
|
+
const layout = computed((): PianoLayout => ({
|
|
102
|
+
noteRange: props.noteRange,
|
|
103
|
+
whiteKeyWidth: props.resizable
|
|
104
|
+
? (resizedKeyWidth.value ?? props.whiteKeyWidth)
|
|
105
|
+
: props.whiteKeyWidth,
|
|
106
|
+
keyGap: props.keyGap,
|
|
107
|
+
blackKeyWidthRatio: props.blackKeyWidthRatio,
|
|
108
|
+
blackKeyHeightRatio: props.blackKeyHeightRatio,
|
|
109
|
+
}))
|
|
110
|
+
const settings = () => ({
|
|
111
|
+
layout: layout.value,
|
|
112
|
+
glissando: props.glissando,
|
|
113
|
+
midiMax: props.midiMax,
|
|
114
|
+
keyboardShortcuts: props.keyboardShortcuts,
|
|
115
|
+
keyboardShortcutsScope: props.keyboardShortcutsScope,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
let instance: PianoInputInstance | null = null
|
|
119
|
+
|
|
120
|
+
// Only the element decides how the instance is wired. Everything else is
|
|
121
|
+
// pushed with update(), so that changing the layout mid-drag — the parent
|
|
122
|
+
// being resized under `resizable`, say — does not abort the drag.
|
|
123
|
+
watch(
|
|
124
|
+
root,
|
|
125
|
+
(element, _, onCleanup) => {
|
|
126
|
+
if (!element) return
|
|
127
|
+
const current = createPianoInput(element, {
|
|
128
|
+
...settings(),
|
|
129
|
+
onPlayNote: (note, velocity) => emit('playNote', note, velocity),
|
|
130
|
+
onStopNote: (note) => emit('stopNote', note),
|
|
131
|
+
onActiveNotesChange: (next) => {
|
|
132
|
+
activeNotes.value = next
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
instance = current
|
|
136
|
+
onCleanup(() => {
|
|
137
|
+
instance = null
|
|
138
|
+
current.destroy()
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
{ immediate: true, flush: 'post' },
|
|
142
|
+
)
|
|
143
|
+
watch(settings, (next) => instance?.update(next))
|
|
144
|
+
|
|
145
|
+
watch(
|
|
146
|
+
() =>
|
|
147
|
+
[
|
|
148
|
+
root.value,
|
|
149
|
+
props.resizable,
|
|
150
|
+
props.noteRange.first,
|
|
151
|
+
props.noteRange.last,
|
|
152
|
+
props.keyGap,
|
|
153
|
+
props.blackKeyWidthRatio,
|
|
154
|
+
] as const,
|
|
155
|
+
(
|
|
156
|
+
[element, resizable, first, last, keyGap, blackKeyWidthRatio],
|
|
157
|
+
_,
|
|
158
|
+
onCleanup,
|
|
159
|
+
) => {
|
|
160
|
+
if (!resizable || !element) return
|
|
161
|
+
const parent = element.parentElement
|
|
162
|
+
if (!parent) throw new Error("doesn't have a parent element.")
|
|
163
|
+
const observer = new ResizeObserver(() => {
|
|
164
|
+
resizedKeyWidth.value = fitWhiteKeyWidth(element.clientWidth, {
|
|
165
|
+
noteRange: { first, last },
|
|
166
|
+
keyGap,
|
|
167
|
+
blackKeyWidthRatio,
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
observer.observe(parent)
|
|
171
|
+
onCleanup(() => observer.disconnect())
|
|
172
|
+
},
|
|
173
|
+
{ immediate: true, flush: 'post' },
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
expose({
|
|
177
|
+
/** Start a note, as a key would. */
|
|
178
|
+
playNote: (note: number, velocity?: number) =>
|
|
179
|
+
instance?.noteOn(note, { source: 'api', velocity }),
|
|
180
|
+
/** Release a note started with `playNote`. */
|
|
181
|
+
stopNote: (note: number) => instance?.noteOff(note, { source: 'api' }),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// The group can own keyboard shortcuts and must receive focus. The width
|
|
185
|
+
// is computed from the layout rather than chosen, so it stays inline.
|
|
186
|
+
return () =>
|
|
187
|
+
h(
|
|
188
|
+
'div',
|
|
189
|
+
{
|
|
190
|
+
ref: root,
|
|
191
|
+
role: 'group',
|
|
192
|
+
tabindex: 0,
|
|
193
|
+
'data-resizable': props.resizable ? '' : undefined,
|
|
194
|
+
style: {
|
|
195
|
+
position: 'relative',
|
|
196
|
+
width: props.resizable ? '100%' : `${pianoWidth(layout.value)}px`,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
notes.value.map((note, index) => {
|
|
200
|
+
const state: KeyState = {
|
|
201
|
+
index,
|
|
202
|
+
keyType: isWhiteKey(note) ? 'white' : 'black',
|
|
203
|
+
active: activeNotes.value.includes(note),
|
|
204
|
+
disabled: note > props.midiMax,
|
|
205
|
+
}
|
|
206
|
+
const { style: keyStyle, ...attributes } =
|
|
207
|
+
props.keyProps?.(note, state) ?? {}
|
|
208
|
+
const white = state.keyType === 'white'
|
|
209
|
+
const label = slots.label?.({ note, state })
|
|
210
|
+
// Placed and sized from the layout; the black keys sit over the
|
|
211
|
+
// white ones whichever order they are drawn in. The geometry comes
|
|
212
|
+
// after the caller's style.
|
|
213
|
+
return h(
|
|
214
|
+
'div',
|
|
215
|
+
{
|
|
216
|
+
key: note,
|
|
217
|
+
'data-note': note,
|
|
218
|
+
'data-note-key': noteKey(note),
|
|
219
|
+
'data-active': state.active ? '' : undefined,
|
|
220
|
+
'data-disabled': state.disabled ? '' : undefined,
|
|
221
|
+
...attributes,
|
|
222
|
+
style: [
|
|
223
|
+
{ position: 'absolute', zIndex: white ? 1 : 2 },
|
|
224
|
+
keyStyle,
|
|
225
|
+
{
|
|
226
|
+
left: `${notePosition(note, layout.value)}px`,
|
|
227
|
+
width: `${white ? layout.value.whiteKeyWidth : blackKeyWidth(layout.value)}px`,
|
|
228
|
+
height: white
|
|
229
|
+
? '100%'
|
|
230
|
+
: `${props.blackKeyHeightRatio * 100}%`,
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
label
|
|
235
|
+
? h('div', { class: props.classes?.keyLabelWrapper }, [
|
|
236
|
+
h('div', { class: props.classes?.keyLabel }, label),
|
|
237
|
+
])
|
|
238
|
+
: undefined,
|
|
239
|
+
)
|
|
240
|
+
}),
|
|
241
|
+
)
|
|
242
|
+
},
|
|
243
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Piano, type KeyAttributes, type KeyState } from './Piano'
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
defineComponent,
|
|
4
|
+
h,
|
|
5
|
+
onScopeDispose,
|
|
6
|
+
provide,
|
|
7
|
+
shallowRef,
|
|
8
|
+
watchEffect,
|
|
9
|
+
type PropType,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
createPointsEditor,
|
|
14
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
15
|
+
POINTS_EDITOR_DEFAULT_KEYBOARD,
|
|
16
|
+
POINTS_EDITOR_DEFAULT_WHEEL,
|
|
17
|
+
type InputEventOption,
|
|
18
|
+
type ModifierValue,
|
|
19
|
+
type SelectionBoxRect,
|
|
20
|
+
} from '@tremolo-ui/dom'
|
|
21
|
+
|
|
22
|
+
import { PointsEditorKey } from './context'
|
|
23
|
+
|
|
24
|
+
const EMPTY: readonly string[] = []
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Multiple point controller. Bind the selection with `v-model:selection` to
|
|
28
|
+
* hold it yourself; the editor keeps its own otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export const PointsEditor = /* @__PURE__ */ defineComponent({
|
|
31
|
+
name: 'PointsEditor',
|
|
32
|
+
props: {
|
|
33
|
+
/** Make the points unchangeable and remove them from the tab order. */
|
|
34
|
+
disabled: Boolean,
|
|
35
|
+
/** Make the points unmovable. */
|
|
36
|
+
readonly: Boolean,
|
|
37
|
+
/** The cursor to show while dragging a point. @default { cursor: 'grabbing' } */
|
|
38
|
+
externalStyles: Object as PropType<{ cursor?: string }>,
|
|
39
|
+
/** How much one wheel notch moves the focused point. */
|
|
40
|
+
wheel: {
|
|
41
|
+
type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
|
|
42
|
+
default: () => POINTS_EDITOR_DEFAULT_WHEEL,
|
|
43
|
+
},
|
|
44
|
+
/** How much one arrow key press moves a point. */
|
|
45
|
+
keyboard: {
|
|
46
|
+
type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
|
|
47
|
+
default: () => POINTS_EDITOR_DEFAULT_KEYBOARD,
|
|
48
|
+
},
|
|
49
|
+
/** How much a drag moves a point, per modifier key. */
|
|
50
|
+
dragSensitivity: {
|
|
51
|
+
type: [Number, Object] as PropType<ModifierValue<number>>,
|
|
52
|
+
default: () => DEFAULT_DRAG_SENSITIVITY,
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Let points be selected, and a selection be moved as one. A selection
|
|
56
|
+
* updates several points in the same tick.
|
|
57
|
+
*/
|
|
58
|
+
selectable: Boolean,
|
|
59
|
+
/** Ids of the selected points. Bind it with `v-model:selection`. */
|
|
60
|
+
selection: Array as PropType<string[]>,
|
|
61
|
+
},
|
|
62
|
+
emits: {
|
|
63
|
+
'update:selection': (selection: string[]) => Array.isArray(selection),
|
|
64
|
+
},
|
|
65
|
+
setup(props, { slots, emit }) {
|
|
66
|
+
const ownSelection = shallowRef<string[]>(props.selection ?? [])
|
|
67
|
+
const selectionBox = shallowRef<SelectionBoxRect | null>(null)
|
|
68
|
+
const container = shallowRef<HTMLElement | null>(null)
|
|
69
|
+
|
|
70
|
+
// Nothing is selected while selection is off, so a drag picks up only
|
|
71
|
+
// the point it started on.
|
|
72
|
+
const current = computed(() =>
|
|
73
|
+
props.selectable ? (props.selection ?? ownSelection.value) : EMPTY,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// The selection lives here, where Vue state can hold it; which points a
|
|
77
|
+
// press or a box selects, and how a selection moves, is the core's.
|
|
78
|
+
const editor = createPointsEditor({
|
|
79
|
+
onSelectionBoxChange: (rect) => {
|
|
80
|
+
selectionBox.value = rect
|
|
81
|
+
},
|
|
82
|
+
onSelectionChange: (next) => {
|
|
83
|
+
ownSelection.value = next
|
|
84
|
+
emit('update:selection', next)
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
watchEffect(
|
|
88
|
+
() =>
|
|
89
|
+
editor.update({
|
|
90
|
+
selectable: props.selectable,
|
|
91
|
+
selection: current.value,
|
|
92
|
+
}),
|
|
93
|
+
{ flush: 'sync' },
|
|
94
|
+
)
|
|
95
|
+
onScopeDispose(() => editor.destroy())
|
|
96
|
+
|
|
97
|
+
provide(PointsEditorKey, {
|
|
98
|
+
get disabled() {
|
|
99
|
+
return props.disabled
|
|
100
|
+
},
|
|
101
|
+
get readonly() {
|
|
102
|
+
return props.readonly
|
|
103
|
+
},
|
|
104
|
+
get wheel() {
|
|
105
|
+
return props.wheel
|
|
106
|
+
},
|
|
107
|
+
get keyboard() {
|
|
108
|
+
return props.keyboard
|
|
109
|
+
},
|
|
110
|
+
get dragSensitivity() {
|
|
111
|
+
return props.dragSensitivity
|
|
112
|
+
},
|
|
113
|
+
get cursor() {
|
|
114
|
+
return props.externalStyles?.cursor ?? 'grabbing'
|
|
115
|
+
},
|
|
116
|
+
get selectable() {
|
|
117
|
+
return props.selectable
|
|
118
|
+
},
|
|
119
|
+
get selection() {
|
|
120
|
+
return current.value
|
|
121
|
+
},
|
|
122
|
+
get selectionBox() {
|
|
123
|
+
return selectionBox.value
|
|
124
|
+
},
|
|
125
|
+
get container() {
|
|
126
|
+
return container.value
|
|
127
|
+
},
|
|
128
|
+
editor,
|
|
129
|
+
setContainer: (element) => {
|
|
130
|
+
container.value = element
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// The layers inside are placed against this box.
|
|
135
|
+
return () =>
|
|
136
|
+
h(
|
|
137
|
+
'div',
|
|
138
|
+
{
|
|
139
|
+
'data-disabled': props.disabled ? '' : undefined,
|
|
140
|
+
'data-readonly': props.readonly ? '' : undefined,
|
|
141
|
+
style: { position: 'relative' },
|
|
142
|
+
},
|
|
143
|
+
slots.default?.(),
|
|
144
|
+
)
|
|
145
|
+
},
|
|
146
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Behind the points, filling the editor. The order of the layers holds
|
|
5
|
+
* whichever way they are written, so the z-index is part of the mechanics.
|
|
6
|
+
*/
|
|
7
|
+
export const PointsEditorBackground = /* @__PURE__ */ defineComponent({
|
|
8
|
+
name: 'PointsEditorBackground',
|
|
9
|
+
setup(_, { slots }) {
|
|
10
|
+
return () =>
|
|
11
|
+
h(
|
|
12
|
+
'div',
|
|
13
|
+
{ style: { position: 'absolute', inset: 0, zIndex: 0 } },
|
|
14
|
+
slots.default?.(),
|
|
15
|
+
)
|
|
16
|
+
},
|
|
17
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { computed, defineComponent, h, ref, watch, watchEffect } from 'vue'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createDragValue,
|
|
5
|
+
elementMapping,
|
|
6
|
+
POINT_AXIS,
|
|
7
|
+
wheelMove,
|
|
8
|
+
} from '@tremolo-ui/dom'
|
|
9
|
+
|
|
10
|
+
import { useWheel } from '../../composables/useWheel'
|
|
11
|
+
import { providePlacement } from '../_util/placement'
|
|
12
|
+
|
|
13
|
+
import { usePointsEditorContext } from './context'
|
|
14
|
+
|
|
15
|
+
/** The layer the points are placed in, over the background. */
|
|
16
|
+
export const PointsEditorContainer = /* @__PURE__ */ defineComponent({
|
|
17
|
+
name: 'PointsEditorContainer',
|
|
18
|
+
setup(_, { slots }) {
|
|
19
|
+
const points = usePointsEditorContext()
|
|
20
|
+
providePlacement('PointsEditorContainer')
|
|
21
|
+
const el = ref<HTMLDivElement | null>(null)
|
|
22
|
+
watchEffect(() => points.setContainer(el.value))
|
|
23
|
+
|
|
24
|
+
// A drag on empty space draws a selection box. It is only attached while
|
|
25
|
+
// there is a selection to draw: `createDrag` puts `touch-action: none` on
|
|
26
|
+
// what it holds, and an editor that cannot select should not stop the
|
|
27
|
+
// page scrolling under a finger.
|
|
28
|
+
const selecting = computed(() => points.selectable && !points.disabled)
|
|
29
|
+
watch(
|
|
30
|
+
[el, selecting],
|
|
31
|
+
([element, on], _, onCleanup) => {
|
|
32
|
+
if (!element || !on) return
|
|
33
|
+
const drag = createDragValue(element, {
|
|
34
|
+
axis: POINT_AXIS,
|
|
35
|
+
mapping: elementMapping(() => element),
|
|
36
|
+
// A press that landed on a point belongs to that point. Declining
|
|
37
|
+
// here rather than later matters: by then the container would
|
|
38
|
+
// already have taken the pointer capture away from the point.
|
|
39
|
+
shouldStart: (event) =>
|
|
40
|
+
!points.editor.isPointElement(event.target as Element | null),
|
|
41
|
+
onDragStart: ([x, y], state) =>
|
|
42
|
+
points.editor.beginSelectionBox({ x, y }, state.event),
|
|
43
|
+
onChange: ([x, y]) => points.editor.moveSelectionBox({ x, y }),
|
|
44
|
+
onDragEnd: () => points.editor.endSelectionBox(),
|
|
45
|
+
})
|
|
46
|
+
onCleanup(() => drag.destroy())
|
|
47
|
+
},
|
|
48
|
+
{ immediate: true, flush: 'post' },
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// One listener for the whole editor rather than one per point: a wheel
|
|
52
|
+
// event only reaches what the cursor is over.
|
|
53
|
+
useWheel(el, (event) => {
|
|
54
|
+
// Scrolling up moves the point towards y = 0; shift switches to x.
|
|
55
|
+
const move = wheelMove(event)
|
|
56
|
+
if (!move) return
|
|
57
|
+
const axis = move.axis === 0 ? 'x' : 'y'
|
|
58
|
+
if (points.editor.nudgeFocusedPoint(axis, move.direction, event)) {
|
|
59
|
+
event.preventDefault()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
return () =>
|
|
64
|
+
h(
|
|
65
|
+
'div',
|
|
66
|
+
{
|
|
67
|
+
ref: el,
|
|
68
|
+
style: { position: 'absolute', inset: 0, zIndex: 10 },
|
|
69
|
+
},
|
|
70
|
+
slots.default?.(),
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
})
|