如果你熟悉 Figma,你会发现输入字段支持通过拖动来调整数值。你不再需要点击输入框输入数字,这个拖动功能非常实用,你可以轻松通过拖动直接得到你想要的数值。
我们可以用 Angular 指令来构建类似的东西。在这个实验里,我们会用到 Angular 的最新特性。
让我们看看怎么建这个。
实际上,我们可以用多种方式来实现这个目标。我们将用指令来构建它,采取一种非常通用的方法。这样一来,我们可以重用这些逻辑,比如调整元素大小或侧边栏等。
清洁剂指令 - 主要功能输入的主要逻辑可以提取并封装成一个指令(或命令),主要目标是监听并响应鼠标事件,然后将鼠标移动转换为可用的数值。具体来说:
当用户按下鼠标(鼠标按下事件)时。
-
我们开始监听到鼠标移动事件,并将它们转换成可用的数值。
当用户松开鼠标时,我们就不再监听。
我们将使用 rxjs
来稍微简化逻辑。
这里就是伪代码的写法。
const mousedown$ = fromEvent<MouseEvent>(target, 'mousedown');
const mousemove$ = fromEvent<MouseEvent>(document, 'mousemove');
const mouseup$ = fromEvent<MouseEvent>(document, 'mouseup');
let startX = 0;
let step = 1;
mousedown$
.pipe(
tap((event) => {
startX = event.clientX; // 鼠标按下时的初始X坐标
}),
switchMap(() => mousemove$.pipe(takeUntil(mouseup$))))
.subscribe((moveEvent) => {
const delta = startX - moveEvent.clientX;
const newValue = Math.round(拖动开始时的初始值 + delta);
});
全屏 退出全屏
查看上述代码,应该很容易看出来发生了什么。我们基本上保存了初始的 clientX
值,这是鼠标点击时 X 轴的坐标。有了这些信息之后,当用户移动鼠标时,我们就可以计算出从初始位置到当前位置的 X 轴偏移值。
我们可以进一步添加更多自定义设置,例如:
-
灵敏度 - 灵敏度决定拖动距离和最终值之间的关系。灵敏度越高,即使只是稍微移动,最终数值也会变得很大。
-
步幅 - 设置移动鼠标时的步进距离。如果步幅设为
1
,则最终值会每次加减1
。 -
Min:最小值。
- Max - 最大发送值。
最终的指令会这样显示:
@Directive({
selector: "[scrubber]",
})
export class ScrubberDirective {
// 定义公共只读的 scrubberTarget 为 input.required<HTMLDivElement>({ 别名为 "scrubber" });
public readonly scrubberTarget = input.required<HTMLDivElement>({
alias: "scrubber",
});
// 定义公共只读的 step 为 model<number>(1);
public readonly step = model<number>(1);
// 定义公共只读的 min 为 model<number>(0);
public readonly min = model<number>(0);
// 定义公共只读的 max 为 model<number>(100);
public readonly max = model<number>(100);
// 定义公共只读的 startValue 为 model(0);
public readonly startValue = model(0);
// 定义公共只读的 sensitivity 为 model(0.1);
public readonly sensitivity = model(0.1);
// 定义公共只读的 scrubbing 为 output<number>();
public readonly scrubbing = output<number>();
// 定义私有的 isDragging 为 signal(false);
private isDragging = signal(false);
// 定义私有的 startX 为 signal(0);
private startX = signal(0);
// 定义私有的 startValueAtTheTimeOfDrag 为 signal(0);
private readonly startValueAtTheTimeOfDrag = signal(0);
// 定义私有的 destroyRef 为 inject(DestroyRef);
private readonly destroyRef = inject(DestroyRef);
// 定义私有的 subs 为 Subscription 类型的可选值
private subs?: Subscription;
constructor() {
// 当 effect 执行时,取消订阅,然后设置订阅
effect(() => {
this.subs?.unsubscribe();
this.subs = this.setupMouseEventListener(this.scrubberTarget());
});
// 当 this.destroyRef 销毁时,移除 "resizing" 类,并取消订阅
this.destroyRef.onDestroy(() => {
document.body.classList.remove("resizing");
this.subs?.unsubscribe();
});
}
// 设置鼠标事件监听器
private setupMouseEventListener(target: HTMLDivElement): Subscription {
// 定义 mousedown$ 为 mousedown 事件
const mousedown$ = fromEvent<MouseEvent>(target, "mousedown");
// 定义 mousemove$ 为 mousemove 事件
const mousemove$ = fromEvent<MouseEvent>(document, "mousemove");
// 定义 mouseup$ 为 mouseup 事件
const mouseup$ = fromEvent<MouseEvent>(document, "mouseup");
// 返回 mousedown$,并设置鼠标按下时的处理函数
return mousedown$
.pipe(
tap((event) => {
// 设置 isDragging 为 true
this.isDragging.set(true);
// 设置 startX 为 event.clientX
this.startX.set(event.clientX);
// 设置 startValueAtTheTimeOfDrag 为 this.startValue()
this.startValueAtTheTimeOfDrag.set(this.startValue());
// 给 document.body 添加一个 "resizing" 类
document.body.classList.add("resizing");
}),
// 切换鼠标移动事件
switchMap(() =>
mousemove$.pipe(
takeUntil(
mouseup$.pipe(
// 设置 isDragging 为 false,并从 document.body 移除 "resizing" 类
tap(() => {
this.isDragging.set(false);
document.body.classList.remove("resizing");
})
)
)
)
)
)
.subscribe((moveEvent) => {
// 计算 delta 和敏感度补偿后的 delta
const delta = moveEvent.clientX - this.startX();
const deltaWithSensitivityCompensation = delta * this.sensitivity();
// 计算新的值
const newValue =
Math.round(
(this.startValueAtTheTimeOfDrag() +
deltaWithSensitivityCompensation) /
this.step()
) * this.step();
// 发出 clampedValue 作为 this.scrubbing 的值
this.emitChange(newValue);
// 设置 startValue 为 newValue
this.startValue.set(newValue);
});
}
// 私有的 emitChange 函数
private emitChange(newValue: number): void {
// 定义常量 clampedValue 为 Math.min(Math.max(newValue, this.min()), this.max());
const clampedValue = Math.min(Math.max(newValue, this.min()), this.max());
// 发出 clampedValue 作为 this.scrubbing 的值
this.scrubbing.emit(clampedValue);
}
}
全屏 退出全屏
使用方法:如何使用 scrubber 功能现在我们已经有了这份指令,让我们看看我们该如何开始使用它。
<div #scrubberTarget
[scrubber]="scrubberTarget"
[startValue]="this.roundedNumericValue()"
[min]="0"
[max]="100"
[step]="2"
[sensitivity]="0.2"
(scrubbing)="this.updateValue($event)">
</div>
点击切换到全屏模式 点击切换出全屏模式
目前,我们将 scrubberTarget
输入标记为必须的,不过实际上,我们可以把它设置为可选的,并自动使用指令宿主元素的 elementRef.nativeElement
,这样效果就跟之前一样。将 scrubberTarget
作为输入暴露出来,这样如果你希望将不同的元素设置为目标,就可以实现这一点。
我们也给 body 添加了一个 resizing
类,以便我们能够正确地设置调整大小的光标。
.resizing {
cursor: ew-resize;
touch-action: none;
-webkit-user-select: none;
user-select: none;
}
全屏 退出全屏
我们使用了 effect
来启动监听器,这样可以确保当目标元素发生变化时,我们会在新元素上重新设置监听器。
我们为 Angular 制作了一个超级简单的数值滑块组件,可以帮助我们构建类似 Figma 中的输入字段,让用户轻松与数值输入互动。
代码和示例
点击此链接查看示例代码:https://stackblitz.com/edit/figma-like-number-input-angular?file=src%2Fscrubber.directive.ts
联系我。推特链接:Twitter
请在评论区留下你的想法。注意安全 ❤️
"给我买个披萨呗"][https://www.buymeacoffee.com/adisreyaj "支持我买个披萨"]
共同学习,写下你的评论
评论加载中...
作者其他优质文章