首頁 >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
上一篇:NPM依賴錯誤下一篇:NPM依賴錯誤