Skip to content

Commit 27cfc2b

Browse files
mondalacicursoragentert78gb
authored
feat: allow direct entry of slider values (#2995)
Closes #2795. Click the value next to any slider to type a number directly, in addition to dragging the slider thumb. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Kiss Róbert <ert78gb@gmail.com>
1 parent 6b519e1 commit 27cfc2b

7 files changed

Lines changed: 173 additions & 19 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
:host {
2-
--slider-value-width: 35px;
2+
--slider-value-width: 50px;
33
}

packages/uhk-web/src/app/components/device/mouse-speed/mouse-speed.component.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ export class MouseSpeedComponent implements OnInit, OnDestroy {
5656
ngOnInit(): void {
5757
this.userConfigSubscription = this.store.select(getUserConfiguration)
5858
.subscribe(config => {
59-
this.mouseMoveInitialSpeed = config.mouseMoveInitialSpeed * MOUSE_MOVE_VALUE_MULTIPLIER || 0;
60-
this.mouseMoveBaseSpeed = config.mouseMoveBaseSpeed * MOUSE_MOVE_VALUE_MULTIPLIER || 0;
61-
this.mouseMoveAcceleration = config.mouseMoveAcceleration * MOUSE_MOVE_VALUE_MULTIPLIER || 0;
62-
this.mouseMoveDeceleratedSpeed = config.mouseMoveDeceleratedSpeed * MOUSE_MOVE_VALUE_MULTIPLIER || 0;
63-
this.mouseMoveAcceleratedSpeed = config.mouseMoveAcceleratedSpeed * MOUSE_MOVE_VALUE_MULTIPLIER || 0;
59+
this.mouseMoveInitialSpeed = this.toDisplayMoveSpeed(config.mouseMoveInitialSpeed);
60+
this.mouseMoveBaseSpeed = this.toDisplayMoveSpeed(config.mouseMoveBaseSpeed);
61+
this.mouseMoveAcceleration = this.toDisplayMoveSpeed(config.mouseMoveAcceleration);
62+
this.mouseMoveDeceleratedSpeed = this.toDisplayMoveSpeed(config.mouseMoveDeceleratedSpeed);
63+
this.mouseMoveAcceleratedSpeed = this.toDisplayMoveSpeed(config.mouseMoveAcceleratedSpeed);
6464
this.mouseMoveAxisSkew = config.mouseMoveAxisSkew || 0;
6565

6666
this.mouseScrollInitialSpeed = config.mouseScrollInitialSpeed || 0;
@@ -101,4 +101,8 @@ export class MouseSpeedComponent implements OnInit, OnDestroy {
101101
resetToMacDefault() {
102102
this.store.dispatch(new ResetMacMouseSpeedSettingsAction());
103103
}
104+
105+
private toDisplayMoveSpeed(stored: number): number {
106+
return Math.round(stored * MOUSE_MOVE_VALUE_MULTIPLIER) || 0;
107+
}
104108
}

packages/uhk-web/src/app/components/device/typing-behavior-page/typing-behavior-page.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
:host {
2-
--slider-value-width: 50px;
2+
--slider-value-width: 60px;
33
--table-td-value-margin-left: 1.75em;
44

55
table.settings {

packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@
2525
(ngModelChange)="onSliderChange($event)"></nouislider>
2626
</div>
2727
<div class="slider-value">
28-
<div class="value-indicator">{{ getFormatedValue(value) }}</div>
28+
<div *ngIf="!editingValue"
29+
class="value-indicator"
30+
[class.editable]="canEditValue"
31+
(click)="startEditing()">{{ getFormatedValue(value) }}</div>
32+
<input *ngIf="editingValue"
33+
#valueInput
34+
class="value-input"
35+
type="number"
36+
[min]="min"
37+
[max]="max"
38+
step="any"
39+
[(ngModel)]="inputValue"
40+
(blur)="commitInputValue()"
41+
(keydown.enter)="$event.preventDefault(); commitInputValue()"
42+
(keydown.escape)="cancelEditing($event)"/>
2943
</div>
3044
</div>

packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,19 @@ $side-value-width: 80px;
2828

2929
.value-indicator {
3030
white-space: nowrap;
31+
32+
&.editable {
33+
cursor: pointer;
34+
35+
&:hover {
36+
text-decoration: underline;
37+
}
38+
}
39+
}
40+
41+
.value-input {
42+
width: 100%;
43+
padding: 0 0.2rem;
44+
text-align: right;
3145
}
3246
}

packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.ts

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { Component, EventEmitter, forwardRef, Input, Output, OnDestroy, ViewChild } from '@angular/core';
1+
import {
2+
Component,
3+
ElementRef,
4+
EventEmitter,
5+
forwardRef,
6+
Input,
7+
OnDestroy,
8+
Output,
9+
ViewChild
10+
} from '@angular/core';
211
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
312
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
413
import { NouisliderComponent } from 'ng2-nouislider';
@@ -31,6 +40,7 @@ export interface SliderProps {
3140
})
3241
export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy {
3342
@ViewChild(NouisliderComponent, { static: false }) slider: NouisliderComponent;
43+
@ViewChild('valueInput') valueInput: ElementRef<HTMLInputElement>;
3444
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3545
@Input() config: any = {};
3646
@Input() label: string;
@@ -50,6 +60,9 @@ export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy {
5060

5161
public value: number;
5262
disabled = false;
63+
editingValue = false;
64+
inputValue: string | number = '';
65+
private cancelPending = false;
5366
private changeObserver$: Observer<number>;
5467
private changeDebounceTime: number = 300;
5568

@@ -62,10 +75,14 @@ export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy {
6275
}
6376
}
6477

78+
get canEditValue(): boolean {
79+
return !this.disabled && !this.valueFormatter;
80+
}
81+
6582
writeValue(value: number): void {
6683
this.value = value === undefined || value === null
6784
? this.min
68-
: value;
85+
: this.normalizeValue(value);
6986
}
7087

7188
registerOnChange(fn: Function): void {
@@ -79,18 +96,124 @@ export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy {
7996
}
8097

8198
getFormatedValue(value: number): string {
99+
const normalized = this.normalizeValue(value);
100+
82101
if (this.valueFormatter) {
83-
return this.valueFormatter(value);
102+
return this.valueFormatter(normalized);
84103
}
85104

86105
if (this.valueUnit) {
87-
return value + ' ' + this.valueUnit;
106+
return normalized + ' ' + this.valueUnit;
107+
}
108+
109+
return normalized.toString(10);
110+
}
111+
112+
startEditing(): void {
113+
if (!this.canEditValue) {
114+
return;
88115
}
89116

90-
return value.toString(10);
117+
this.cancelPending = false;
118+
this.inputValue = this.normalizeValue(this.value).toString(10);
119+
this.editingValue = true;
120+
121+
setTimeout(() => {
122+
const input = this.valueInput?.nativeElement;
123+
if (input) {
124+
input.focus();
125+
input.select();
126+
}
127+
});
128+
}
129+
130+
commitInputValue(): void {
131+
if (this.cancelPending) {
132+
this.cancelPending = false;
133+
this.editingValue = false;
134+
return;
135+
}
136+
137+
if (!this.editingValue) {
138+
return;
139+
}
140+
141+
const parsed = this.parseInputValue(this.inputValue);
142+
this.editingValue = false;
143+
144+
if (parsed === null) {
145+
return;
146+
}
147+
148+
const clamped = this.normalizeValue(Math.min(this.max, Math.max(this.min, parsed)));
149+
this.value = clamped;
150+
this.propagateValueChange(clamped);
151+
}
152+
153+
cancelEditing(event?: KeyboardEvent): void {
154+
if (event) {
155+
event.preventDefault();
156+
event.stopPropagation();
157+
}
158+
159+
this.cancelPending = true;
160+
this.editingValue = false;
91161
}
92162

93163
onSliderChange(value: number): void {
164+
this.propagateValueChange(value, true);
165+
}
166+
167+
htmlTooltip(): SafeHtml {
168+
return this.sanitizer.bypassSecurityTrustHtml(this.tooltip);
169+
}
170+
171+
private parseInputValue(raw: string | number | null | undefined): number | null {
172+
if (raw === null || raw === undefined) {
173+
return null;
174+
}
175+
176+
if (typeof raw === 'number') {
177+
return Number.isFinite(raw) ? raw : null;
178+
}
179+
180+
if (!raw.trim()) {
181+
return null;
182+
}
183+
184+
let trimmed = raw.trim();
185+
if (this.valueUnit) {
186+
const suffix = ' ' + this.valueUnit;
187+
if (trimmed.endsWith(suffix)) {
188+
trimmed = trimmed.slice(0, -suffix.length).trim();
189+
}
190+
}
191+
192+
const parsed = Number(trimmed);
193+
return Number.isFinite(parsed) ? parsed : null;
194+
}
195+
196+
private getStepDecimalPlaces(): number {
197+
if (!this.step || this.step <= 0) {
198+
return 0;
199+
}
200+
201+
const stepString = this.step.toString();
202+
const decimalIndex = stepString.indexOf('.');
203+
if (decimalIndex === -1) {
204+
return 0;
205+
}
206+
207+
return stepString.length - decimalIndex - 1;
208+
}
209+
210+
private normalizeValue(value: number): number {
211+
const stepDecimals = this.getStepDecimalPlaces();
212+
const fractionDigits = Math.min(Math.max(stepDecimals + 2, 6), 10);
213+
return Number(value.toFixed(fractionDigits));
214+
}
215+
216+
private propagateValueChange(value: number, skipFirstChange = false): void {
94217
if (!this.changeObserver$) {
95218
Observable.create(observer => {
96219
this.changeObserver$ = observer;
@@ -99,13 +222,12 @@ export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy {
99222
distinctUntilChanged()
100223
).subscribe(this.propagateChange);
101224

102-
return; // No change event on first change as the value is just being set
225+
if (skipFirstChange) {
226+
return;
227+
}
103228
}
104-
this.changeObserver$.next(value);
105-
}
106229

107-
htmlTooltip(): SafeHtml {
108-
return this.sanitizer.bypassSecurityTrustHtml(this.tooltip);
230+
this.changeObserver$.next(value);
109231
}
110232

111233
private propagateChange: Function = () => {};

packages/uhk-web/src/styles/_table.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ table.settings {
7575
}
7676

7777
slider-wrapper {
78-
--slider-value-width: 45px;
78+
--slider-value-width: 55px;
7979
display: block;
8080
margin-left: -1.25rem;
8181
margin-right: 2rem;

0 commit comments

Comments
 (0)