首页 >web前端 >js教程 >Figma 类似于 Angular 中使用指令的输入字段

Figma 类似于 Angular 中使用指令的输入字段

Linda Hamilton
Linda Hamilton原创
2025-01-02 21:00:40799浏览

熟悉 Figma 的人会注意到,输入字段支持拖动来增加或减少值。拖动功能非常方便,您可以通过拖动轻松获得所需的值,而不必先单击输入字段,然后输入数字。

我们可以使用 Angular 指令构建类似的东西。我们将在本实验中使用 Angular 的所有最新功能。

Figma like input field in Angular using Directives

让我们看看如何构建它。

我们实际上可以通过多种方式做到这一点。我们将使用指令来构建它。我们要做到这一点的方法是采用一种非常通用的方法。这样,我们就可以重用逻辑来调整元素或侧边栏的大小等。

洗涤器指令 - 核心功能

输入的主要逻辑可以被提取并封装成指令。主要目标是监听鼠标事件,然后将鼠标移动转换为可用值。更详细地解释一下:

  1. 当用户单击鼠标时(mousedown 事件)。

  2. 我们开始监听鼠标移动(mousemove 事件)并使用该信息将其转换为可用值。

  3. 当用户释放点击时,我们停止监听器(mouseup 事件)。

我们将使用 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; // Initial x co-ordinate where the mouse down happened
    }),
    switchMap(() => mousemove$.pipe(takeUntil(mouseup$))))
  .subscribe((moveEvent) => {
    const delta = startX - moveEvent.clientX;
    const newValue = Math.round(startValueAtTheTimeOfDrag + delta);
  });

看看上面的代码,应该很清楚发生了什么。我们基本上保存了初始的clientX值,即点击在X轴上的位置。一旦我们有了这些信息,当用户移动鼠标时,我们就可以计算初始起始位置和当前 X 位置的增量。

我们可以进一步添加更多自定义项,例如:

  1. 灵敏度 - 拖动到最终值的距离将由灵敏度决定。灵敏度值越高,即使移动幅度不大,最终的值也会很大。

  2. 步数 - 设置移动鼠标时的步进间隔。如果步长值为 1,则最终值以 1 为步长递增/递减。

  3. Min - 将发出的最小值。

  4. Max - 将发出的最大值。

最终指令如下所示:

@Directive({
  selector: "[scrubber]",
})
export class ScrubberDirective {
  public readonly scrubberTarget = input.required<HTMLDivElement>({
    alias: "scrubber",
  });

  public readonly step = model<number>(1);
  public readonly min = model<number>(0);
  public readonly max = model<number>(100);
  public readonly startValue = model(0);
  public readonly sensitivity = model(0.1);

  public readonly scrubbing = output<number>();

  private isDragging = signal(false);
  private startX = signal(0);
  private readonly startValueAtTheTimeOfDrag = signal(0);
  private readonly destroyRef = inject(DestroyRef);
  private subs?: Subscription;

  constructor() {
    effect(() => {
      this.subs?.unsubscribe();
      this.subs = this.setupMouseEventListener(this.scrubberTarget());
    });

    this.destroyRef.onDestroy(() => {
      document.body.classList.remove('resizing');
      this.subs?.unsubscribe();
    });
  }

  private setupMouseEventListener(target: HTMLDivElement): Subscription {
    const mousedown$ = fromEvent<MouseEvent>(target, "mousedown");
    const mousemove$ = fromEvent<MouseEvent>(document, "mousemove");
    const mouseup$ = fromEvent<MouseEvent>(document, "mouseup");

    return mousedown$
      .pipe(
        tap((event) => {
          this.isDragging.set(true);
          this.startX.set(event.clientX);
          this.startValueAtTheTimeOfDrag.set(this.startValue());
          document.body.classList.add("resizing");
        }),
        switchMap(() =>
          mousemove$.pipe(
            takeUntil(
              mouseup$.pipe(
                tap(() => {
                  this.isDragging.set(false);
                  document.body.classList.remove("resizing");
                })
              )
            )
          )
        )
      )
      .subscribe((moveEvent) => {
        const delta = moveEvent.clientX - this.startX();
        const deltaWithSensitivityCompensation = delta * this.sensitivity();

        const newValue =
          Math.round(
            (this.startValueAtTheTimeOfDrag() +
              deltaWithSensitivityCompensation) /
              this.step()
          ) * this.step();

        this.emitChange(newValue);
        this.startValue.set(newValue);
      });
  }

  private emitChange(newValue: number): void {
    const clampedValue = Math.min(Math.max(newValue, this.min()), this.max());
    this.scrubbing.emit(clampedValue);
  }
}

如何使用清理器指令

现在我们已经准备好了指令,让我们看看如何实际开始使用它。

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; // Initial x co-ordinate where the mouse down happened
    }),
    switchMap(() => mousemove$.pipe(takeUntil(mouseup$))))
  .subscribe((moveEvent) => {
    const delta = startX - moveEvent.clientX;
    const newValue = Math.round(startValueAtTheTimeOfDrag + delta);
  });

目前,我们已将scrubberTarget输入标记为input.required,但实际上我们可以将其设为可选并自动使用指令宿主的elementRef.nativeElement,并且它的工作原理相同。如果您想将不同的元素设置为目标,scrubberTarget 将作为输入公开。

我们还在正文中添加了一个调整大小的类,以便我们可以正确设置调整大小光标。

@Directive({
  selector: "[scrubber]",
})
export class ScrubberDirective {
  public readonly scrubberTarget = input.required<HTMLDivElement>({
    alias: "scrubber",
  });

  public readonly step = model<number>(1);
  public readonly min = model<number>(0);
  public readonly max = model<number>(100);
  public readonly startValue = model(0);
  public readonly sensitivity = model(0.1);

  public readonly scrubbing = output<number>();

  private isDragging = signal(false);
  private startX = signal(0);
  private readonly startValueAtTheTimeOfDrag = signal(0);
  private readonly destroyRef = inject(DestroyRef);
  private subs?: Subscription;

  constructor() {
    effect(() => {
      this.subs?.unsubscribe();
      this.subs = this.setupMouseEventListener(this.scrubberTarget());
    });

    this.destroyRef.onDestroy(() => {
      document.body.classList.remove('resizing');
      this.subs?.unsubscribe();
    });
  }

  private setupMouseEventListener(target: HTMLDivElement): Subscription {
    const mousedown$ = fromEvent<MouseEvent>(target, "mousedown");
    const mousemove$ = fromEvent<MouseEvent>(document, "mousemove");
    const mouseup$ = fromEvent<MouseEvent>(document, "mouseup");

    return mousedown$
      .pipe(
        tap((event) => {
          this.isDragging.set(true);
          this.startX.set(event.clientX);
          this.startValueAtTheTimeOfDrag.set(this.startValue());
          document.body.classList.add("resizing");
        }),
        switchMap(() =>
          mousemove$.pipe(
            takeUntil(
              mouseup$.pipe(
                tap(() => {
                  this.isDragging.set(false);
                  document.body.classList.remove("resizing");
                })
              )
            )
          )
        )
      )
      .subscribe((moveEvent) => {
        const delta = moveEvent.clientX - this.startX();
        const deltaWithSensitivityCompensation = delta * this.sensitivity();

        const newValue =
          Math.round(
            (this.startValueAtTheTimeOfDrag() +
              deltaWithSensitivityCompensation) /
              this.step()
          ) * this.step();

        this.emitChange(newValue);
        this.startValue.set(newValue);
      });
  }

  private emitChange(newValue: number): void {
    const clampedValue = Math.min(Math.max(newValue, this.min()), this.max());
    this.scrubbing.emit(clampedValue);
  }
}

我们使用effect来启动监听器,这将确保当目标元素发生变化时,我们在新元素上设置监听器。

查看实际效果

Figma like input field in Angular using Directives

我们在 Angular 中制作了一个超级简单的 Scrubber 指令,它可以帮助我们构建类似于 Figma 的输入字段。使用户与数字输入交互变得非常容易。

代码与演示

https://stackblitz.com/edit/figma-like-number-input-angular?file=src/scrubber.directive.ts

与我联系

  • 推特

  • Github

  • Linkedin

请在评论部分添加您的想法。确保安全❤️

Figma like input field in Angular using Directives

以上是Figma 类似于 Angular 中使用指令的输入字段的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn