1
1
<template >
2
2
<div class =" persistent-scrollbar" :class =" direction.toLowerCase()" >
3
- <button class =" arrow decrease" ></button >
4
- <div class =" scroll-track" >
5
- <div class =" scroll-click-area decrease" :style =" [trackStart, preThumb, sides]" ></div >
6
- <div class =" scroll-thumb" :style =" [thumbStart, thumbEnd, sides]" ></div >
7
- <div class =" scroll-click-area increase" :style =" [postThumb, trackEnd, sides]" ></div >
3
+ <button class =" arrow decrease" @mousedown =" changePosition(-50)" ></button >
4
+ <div class =" scroll-track" ref =" scrollTrack" @mousedown =" grabArea" >
5
+ <div class =" scroll-thumb" @mousedown =" grabHandle" :class =" { dragging }" ref =" handle" :style =" [thumbStart, thumbEnd, sides]" ></div >
8
6
</div >
9
- <button class =" arrow increase" ></button >
7
+ <button class =" arrow increase" @click = " changePosition(50) " ></button >
10
8
</div >
11
9
</template >
12
10
39
37
& :hover {
40
38
background : var (--color-6-lowergray );
41
39
}
40
+ & .dragging {
41
+ background : var (--color-accent-hover );
42
+ }
42
43
}
43
44
44
45
.scroll-click-area {
57
58
& :hover {
58
59
border-color : transparent transparent var (--color-6-lowergray ) transparent ;
59
60
}
61
+ & :active {
62
+ border-color : transparent transparent var (--color-c-brightgray ) transparent ;
63
+ }
60
64
}
61
65
62
66
.arrow.increase {
67
71
& :hover {
68
72
border-color : var (--color-6-lowergray ) transparent transparent transparent ;
69
73
}
74
+ & :active {
75
+ border-color : var (--color-c-brightgray ) transparent transparent transparent ;
76
+ }
70
77
}
71
78
}
72
79
81
88
& :hover {
82
89
border-color : transparent var (--color-6-lowergray ) transparent transparent ;
83
90
}
91
+ & :active {
92
+ border-color : transparent var (--color-c-brightgray ) transparent transparent ;
93
+ }
84
94
}
85
95
86
96
.arrow.increase {
91
101
& :hover {
92
102
border-color : transparent transparent transparent var (--color-6-lowergray );
93
103
}
104
+ & :active {
105
+ border-color : transparent transparent transparent var (--color-c-brightgray );
106
+ }
94
107
}
95
108
}
96
109
}
99
112
<script lang="ts">
100
113
import { defineComponent , PropType } from " vue" ;
101
114
115
+ // Linear Interpolation
116
+ const lerp = (x : number , y : number , a : number ) => x * (1 - a ) + y * a ;
117
+
118
+ // Convert the position of the handle (0-1) to the position on the track (0-1).
119
+ // This includes the 1/2 handle length gap of the possible handle positionson each side so the end of the handle doesn't go off the track.
120
+ const handleToTrack = (handleLen : number , handlePos : number ) => lerp (handleLen / 2 , 1 - handleLen / 2 , handlePos );
121
+
122
+ const mousePosition = (direction : ScrollbarDirection , e : MouseEvent ) => (direction === ScrollbarDirection .Vertical ? e .clientY : e .clientX );
123
+
102
124
export enum ScrollbarDirection {
103
125
" Horizontal" = " Horizontal" ,
104
126
" Vertical" = " Vertical" ,
@@ -107,33 +129,19 @@ export enum ScrollbarDirection {
107
129
export default defineComponent ({
108
130
props: {
109
131
direction: { type: String as PropType <ScrollbarDirection >, default: ScrollbarDirection .Vertical },
132
+ handlePosition: { type: Number , default: 0.5 },
133
+ handleLength: { type: Number , default: 0.5 },
110
134
},
111
135
computed: {
112
- trackStart(): { left: string } | { top: string } {
113
- return this .direction === ScrollbarDirection .Vertical ? { top: " 0%" } : { left: " 0%" };
114
- },
115
- preThumb(): { right: string } | { bottom: string } {
116
- const start = 25 ;
117
-
118
- return this .direction === ScrollbarDirection .Vertical ? { bottom: ` ${100 - start }% ` } : { right: ` ${100 - start }% ` };
119
- },
120
136
thumbStart(): { left: string } | { top: string } {
121
- const start = 25 ;
137
+ const start = handleToTrack ( this . handleLength , this . handlePosition ) - this . handleLength / 2 ;
122
138
123
- return this .direction === ScrollbarDirection .Vertical ? { top: ` ${start }% ` } : { left: ` ${start }% ` };
139
+ return this .direction === ScrollbarDirection .Vertical ? { top: ` ${start * 100 }% ` } : { left: ` ${start * 100 }% ` };
124
140
},
125
141
thumbEnd(): { right: string } | { bottom: string } {
126
- const end = 25 ;
142
+ const end = 1 - handleToTrack ( this . handleLength , this . handlePosition ) - this . handleLength / 2 ;
127
143
128
- return this .direction === ScrollbarDirection .Vertical ? { bottom: ` ${end }% ` } : { right: ` ${end }% ` };
129
- },
130
- postThumb(): { left: string } | { top: string } {
131
- const end = 25 ;
132
-
133
- return this .direction === ScrollbarDirection .Vertical ? { top: ` ${100 - end }% ` } : { left: ` ${100 - end }% ` };
134
- },
135
- trackEnd(): { right: string } | { bottom: string } {
136
- return this .direction === ScrollbarDirection .Vertical ? { bottom: " 0%" } : { right: " 0%" };
144
+ return this .direction === ScrollbarDirection .Vertical ? { bottom: ` ${end * 100 }% ` } : { right: ` ${end * 100 }% ` };
137
145
},
138
146
sides(): { left: string ; right: string } | { top: string ; bottom: string } {
139
147
return this .direction === ScrollbarDirection .Vertical ? { left: " 0%" , right: " 0%" } : { top: " 0%" , bottom: " 0%" };
@@ -142,7 +150,58 @@ export default defineComponent({
142
150
data() {
143
151
return {
144
152
ScrollbarDirection ,
153
+ dragging: false ,
154
+ mousePos: 0 ,
145
155
};
146
156
},
157
+ mounted() {
158
+ window .addEventListener (" mouseup" , () => {
159
+ this .dragging = false ;
160
+ });
161
+ window .addEventListener (" mousemove" , this .mouseMove );
162
+ },
163
+ methods: {
164
+ trackLength(): number {
165
+ const track = this .$refs .scrollTrack as HTMLElement ;
166
+ return this .direction === ScrollbarDirection .Vertical ? track .clientHeight - this .handleLength : track .clientWidth ;
167
+ },
168
+ trackOffset(): number {
169
+ const track = this .$refs .scrollTrack as HTMLElement ;
170
+ return this .direction === ScrollbarDirection .Vertical ? track .getBoundingClientRect ().top : track .getBoundingClientRect ().left ;
171
+ },
172
+ clampHandlePosition(newPos : number ) {
173
+ const clampedPosition = Math .min (Math .max (newPos , 0 ), 1 );
174
+ this .$emit (" update:handlePosition" , clampedPosition );
175
+ },
176
+ updateHandlePosition(e : MouseEvent ) {
177
+ const position = mousePosition (this .direction , e );
178
+ this .clampHandlePosition (this .handlePosition + (position - this .mousePos ) / (this .trackLength () * (1 - this .handleLength )));
179
+ this .mousePos = position ;
180
+ },
181
+ grabHandle(e : MouseEvent ) {
182
+ if (! this .dragging ) {
183
+ this .dragging = true ;
184
+ this .mousePos = mousePosition (this .direction , e );
185
+ }
186
+ },
187
+ grabArea(e : MouseEvent ) {
188
+ if (! this .dragging ) {
189
+ this .dragging = true ;
190
+ this .mousePos = mousePosition (this .direction , e );
191
+ this .clampHandlePosition (((this .mousePos - this .trackOffset ()) / this .trackLength () - this .handleLength / 2 ) / (1 - this .handleLength ));
192
+ }
193
+ },
194
+ mouseUp() {
195
+ this .dragging = false ;
196
+ },
197
+ mouseMove(e : MouseEvent ) {
198
+ if (this .dragging ) {
199
+ this .updateHandlePosition (e );
200
+ }
201
+ },
202
+ changePosition(difference : number ) {
203
+ this .clampHandlePosition (this .handlePosition + difference / this .trackLength ());
204
+ },
205
+ },
147
206
});
148
207
</script >
0 commit comments