Erstellen Sie eine Direktive zum freien Ziehen in Angular

In diesem Artikel lernen wir, wie man eine Direktive in Angular erstellt, die es uns ermöglicht, jedes Element frei zu ziehen, ohne Bibliotheken von Drittanbietern zu verwenden.

Beginnen wir mit dem Programmieren

1 Erstellen Sie eine einfache Anweisung zum freien Ziehen

Wir beginnen mit der Erstellung einer grundlegenden und einfachen Anweisung und werden dann weitere Funktionen hinzufügen.

1.1 Erstellen Sie einen Arbeitsbereich

npm i -g @angular/cli
ng new angular-free-dragging --defaults --minimal

1.2 Geteiltes Modul erstellen

ng g m shared

1.3.1 Freie Ziehanweisungen erstellen

ng g d shared/free-dragging

1.3.2 Direktive exportieren

Sobald es erstellt ist, fügen Sie es dem exports-Array von shared hinzu Modul:

// src/app/shared/shared.module.ts

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FreeDraggingDirective } from "./free-dragging.directive";

@NgModule({
  declarations: [FreeDraggingDirective],
  imports: [CommonModule],
  exports: [FreeDraggingDirective], // Added
})
export class SharedModule {}

1.3.3 Freie Ziehlogik

Um ein freies Ziehen zu haben, gehen wir wie folgt vor:

  1. Hören Sie auf mousedown Ereignis auf Element. Dies funktioniert als Drag-Start auslösen.
  2. Hören Sie auf mousemove Ereignis auf Dokument. Dies funktioniert als Ziehen Abzug. Es aktualisiert auch die Position des Elements basierend auf dem Mauszeiger.
  3. Hören Sie auf mouseup Ereignis auf Dokument. Dies funktioniert als Ziehen-Ende Abzug. Damit hören wir nicht mehr auf mousemove Veranstaltung.

Für alle oben genannten Zuhörer werden wir Observables erstellen. Aber zuerst richten wir unsere Direktive ein:

// src/app/shared/free-dragging.directive.ts

@Directive({
  selector: "[appFreeDragging]",
})
export class FreeDraggingDirective implements OnInit, OnDestroy {
  private element: HTMLElement;

  private subscriptions: Subscription[] = [];

  constructor(
    private elementRef: ElementRef,
    @Inject(DOCUMENT) private document: any
  ) {}

  ngOnInit(): void {
    this.element = this.elementRef.nativeElement as HTMLElement;
    this.initDrag();
  }

  initDrag(): void {
    // main logic will come here
  }

  ngOnDestroy(): void {
    this.subscriptions.forEach((s) => s.unsubscribe());
  }
}

Im obigen Code machen wir hauptsächlich 3 Dinge:

  1. Natives HTML-Element erhalten, damit wir seine Position später ändern können.
  2. Initiieren aller Ziehvorgänge, wir werden dies bald im Detail sehen.
  3. Zum Zeitpunkt der Zerstörung kündigen wir das Abonnement, um Ressourcen freizugeben.

Lassen Sie uns Ziehfunktionen schreiben:

// src/app/shared/free-dragging.directive.ts

...

  initDrag(): void {
    // 1
    const dragStart$ = fromEvent<MouseEvent>(this.element, "mousedown");
    const dragEnd$ = fromEvent<MouseEvent>(this.document, "mouseup");
    const drag$ = fromEvent<MouseEvent>(this.document, "mousemove").pipe(
      takeUntil(dragEnd$)
    );

    // 2
    let initialX: number,
      initialY: number,
      currentX = 0,
      currentY = 0;

    let dragSub: Subscription;

    // 3
    const dragStartSub = dragStart$.subscribe((event: MouseEvent) => {
      initialX = event.clientX - currentX;
      initialY = event.clientY - currentY;
      this.element.classList.add('free-dragging');

      // 4
      dragSub = drag$.subscribe((event: MouseEvent) => {
        event.preventDefault();

        currentX = event.clientX - initialX;
        currentY = event.clientY - initialY;

        this.element.style.transform =
          "translate3d(" + currentX + "px, " + currentY + "px, 0)";
      });
    });

    // 5
    const dragEndSub = dragEnd$.subscribe(() => {
      initialX = currentX;
      initialY = currentY;
      this.element.classList.remove('free-dragging');
      if (dragSub) {
        dragSub.unsubscribe();
      }
    });

    // 6
    this.subscriptions.push.apply(this.subscriptions, [
      dragStartSub,
      dragSub,
      dragEndSub,
    ]);
  }

...
  1. Wir erstellen 3 Observables für die Zuhörer, die wir zuvor mit [fromEvent](https://rxjs.dev/api/index/function/fromEvent) gesehen haben Funktion.
  2. Dann erstellen wir einige Hilfsvariablen, die beim Aktualisieren der Position unseres Elements benötigt werden.
  3. Als nächstes hören wir auf mousedown Veranstaltung auf unserem Element. Sobald der Benutzer die Maus drückt, speichern wir die Anfangsposition und fügen auch eine Klasse free-dragging hinzu was dem Element einen schönen Schatten hinzufügt.
  4. Wir möchten das Element nur verschieben, wenn der Benutzer darauf geklickt hat, deshalb warten wir auf mousemove Ereignis innerhalb des Abonnenten von mousedown Veranstaltung. Wenn der Benutzer die Maus bewegt, aktualisieren wir auch seine Position mit der Transformationseigenschaft.
  5. Wir hören dann auf mouseup Veranstaltung. Hier aktualisieren wir erneut die Anfangspositionen, damit das nächste Ziehen von hier aus erfolgt. Und wir entfernen den free-dragging Klasse.
  6. Schließlich pushen wir alle Abonnements, damit wir uns von allen in ngOnDestroy abmelden können .

Es ist an der Zeit, dies in AppComponent. auszuprobieren

1.3.4 App-Komponente aktualisieren

Ersetzen Sie den Inhalt durch Folgendes:

// src/app/app.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  // 1 use directive
  template: ` <div class="example-box" appFreeDragging>Drag me around</div> `,
  // 2 some helper styles
  styles: [
    `
      .example-box {
        width: 200px;
        height: 200px;
        border: solid 1px #ccc;
        color: rgba(0, 0, 0, 0.87);
        cursor: move;
        display: flex;
        justify-content: center;
        align-items: center;
        text-align: center;
        background: #fff;
        border-radius: 4px;
        position: relative;
        z-index: 1;
        transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
        box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
          0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
      }

      .example-box.free-dragging {
        box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
          0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
      }
    `,
  ],
})
export class AppComponent {}

Der obige Code ist einfach und klar genug. Lassen Sie es uns ausführen:

ng serve

und sehen Sie sich die Ausgabe an:

In der aktuellen Richtlinie kann der Benutzer ein Element ziehen, indem er die Maus irgendwo im Element drückt und bewegt. Der Nachteil davon ist, dass andere Aktionen, wie das Auswählen des Textes, schwierig sind. Und in praktischeren Szenarien, wie z. B. Widgets, benötigen Sie einen Griff, um das Ziehen zu erleichtern.

2. Unterstützung für Drag Handle hinzugefügt

Wir werden Unterstützung für Ziehgriffe hinzufügen, indem wir eine weitere Direktive erstellen und mit @ContentChild darauf zugreifen in unserer Hauptrichtlinie.

2.1 Erstellen Sie eine Anweisung für den Ziehgriff

ng g d shared/free-dragging-handle

2.2 Exportieren Sie es aus dem freigegebenen Modul

// src/app/shared/shared.module.ts

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FreeDraggingDirective } from "./free-dragging.directive";
import { FreeDraggingHandleDirective } from './free-dragging-handle.directive';

@NgModule({
  declarations: [FreeDraggingDirective, FreeDraggingHandleDirective],
  imports: [CommonModule],
  exports: [FreeDraggingDirective, FreeDraggingHandleDirective], // Modified
})
export class SharedModule {}

2.3 ElementRef vom Ziehgriff zurückgeben

Wir brauchen nur das Element des Ziehgriffs, um die nächsten Dinge zu erledigen, verwenden wir ElementRef für das Selbe:

// src/app/shared/free-dragging-handle.directive.ts

import { Directive, ElementRef } from "@angular/core";

@Directive({
  selector: "[appFreeDraggingHandle]",
})
export class FreeDraggingHandleDirective {
  constructor(public elementRef: ElementRef<HTMLElement>) {} // Modified
}

2.4 Ziehen mit Griff

Die Logik geht so:

  1. Kind-Ziehgriff-Element vom Hauptelement holen
  2. Hören Sie auf mousedown Ereignis auf Handle-Element. Dies funktioniert als Drag-Start auslösen.
  3. Hören Sie auf mousemove Ereignis auf Dokument. Dies funktioniert als Ziehen Abzug. Es aktualisiert auch die Position des Hauptelements (und nicht nur des Handle-Elements) basierend auf dem Mauszeiger.
  4. Hören Sie auf mouseup Ereignis auf Dokument. Dies funktioniert als Ziehen-Ende Abzug. Damit hören wir nicht mehr auf mousemove Veranstaltung.

Im Grunde wäre also die einzige Änderung, das Element zu ändern, auf dem wir auf mousedown hören werden Veranstaltung.

Kommen wir zurück zur Codierung:

// src/app/shared/free-dragging.directive.ts

...

@Directive({
  selector: "[appFreeDragging]",
})
export class FreeDraggingDirective implements AfterViewInit, OnDestroy {

  private element: HTMLElement;

  private subscriptions: Subscription[] = [];

  // 1 Added
  @ContentChild(FreeDraggingHandleDirective) handle: FreeDraggingHandleDirective;
  handleElement: HTMLElement;

  constructor(...) {}

  // 2 Modified
  ngAfterViewInit(): void {
    this.element = this.elementRef.nativeElement as HTMLElement;
    this.handleElement = this.handle?.elementRef?.nativeElement || this.element;
    this.initDrag();
  }

  initDrag(): void {
    // 3 Modified
    const dragStart$ = fromEvent<MouseEvent>(this.handleElement, "mousedown");

    // rest remains same

  }

  ...

}

Wir machen dasselbe wie das, was in der Logik vor dem Code erklärt wird. Bitte beachten Sie, dass jetzt statt ngOnInit wir verwenden ngAfterViewInit , weil wir sicherstellen wollen, dass die Ansicht der Komponente vollständig initialisiert ist und wir den FreeDraggingDirective erhalten können Falls vorhanden. Sie können mehr darüber unter Angular - Hooking into the component lifecycle lesen.

2.5 App-Komponente aktualisieren

// src/app/app.component.ts

@Component({
  selector: "app-root",
  template: `
    <!-- 1 use directive -->
    <div class="example-box" appFreeDragging>
      I can only be dragged using the handle

      <!-- 2 use handle directive -->
      <div class="example-handle" appFreeDraggingHandle>
        <svg width="24px" fill="currentColor" viewBox="0 0 24 24">
          <path
            d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"
          ></path>
          <path d="M0 0h24v24H0z" fill="none"></path>
        </svg>
      </div>
    </div>
  `,
  // 3 helper styles
  styles: [
    `
      .example-box {
        width: 200px;
        height: 200px;
        padding: 10px;
        box-sizing: border-box;
        border: solid 1px #ccc;
        color: rgba(0, 0, 0, 0.87);
        display: flex;
        justify-content: center;
        align-items: center;
        text-align: center;
        background: #fff;
        border-radius: 4px;
        position: relative;
        z-index: 1;
        transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
        box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
          0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
      }

      .example-box.free-dragging {
        box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
          0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
      }

      .example-handle {
        position: absolute;
        top: 10px;
        right: 10px;
        color: #ccc;
        cursor: move;
        width: 24px;
        height: 24px;
      }
    `,
  ],
})
export class AppComponent {}

Schauen wir uns die Ausgabe an:

Großartig, wir haben fast erreicht, was wir brauchen.

Aber es gibt immer noch ein Problem damit. Es erlaubt dem Benutzer, Elemente außerhalb der Ansicht zu verschieben:

3. Unterstützung für das Ziehen von Grenzen hinzugefügt

Es ist an der Zeit, Unterstützung für Grenzen hinzuzufügen. Die Grenze hilft dem Benutzer, das Element innerhalb des gewünschten Bereichs zu halten.

3.1 Aktualisierung der Richtlinie

Für die Grenzunterstützung gehen wir wie folgt vor:

  1. Fügen Sie einen @Input hinzu um eine benutzerdefinierte Begrenzungselementabfrage festzulegen. Standardmäßig behalten wir es bei body .
  2. Prüfen Sie, ob wir das Begrenzungselement mit querySelector erhalten können , wenn nicht Fehler werfen.
  3. Verwenden Sie die Layouthöhe und -breite des Begrenzungselements, um die Position des gezogenen Elements anzupassen.
// src/app/shared/free-dragging.directive.ts

...

@Directive({
  selector: "[appFreeDragging]",
})
export class FreeDraggingDirective implements AfterViewInit, OnDestroy {

  ...

  // 1 Added
  private readonly DEFAULT_DRAGGING_BOUNDARY_QUERY = "body";
  @Input() boundaryQuery = this.DEFAULT_DRAGGING_BOUNDARY_QUERY;
  draggingBoundaryElement: HTMLElement | HTMLBodyElement;

  ...

  // 2 Modified
  ngAfterViewInit(): void {
    this.draggingBoundaryElement = (this.document as Document).querySelector(
      this.boundaryQuery
    );
    if (!this.draggingBoundaryElement) {
      throw new Error(
        "Couldn't find any element with query: " + this.boundaryQuery
      );
    } else {
      this.element = this.elementRef.nativeElement as HTMLElement;
      this.handleElement =
        this.handle?.elementRef?.nativeElement || this.element;
      this.initDrag();
    }
  }

  initDrag(): void {
    ...

    // 3 Min and max boundaries
    const minBoundX = this.draggingBoundaryElement.offsetLeft;
    const minBoundY = this.draggingBoundaryElement.offsetTop;
    const maxBoundX =
      minBoundX +
      this.draggingBoundaryElement.offsetWidth -
      this.element.offsetWidth;
    const maxBoundY =
      minBoundY +
      this.draggingBoundaryElement.offsetHeight -
      this.element.offsetHeight;

    const dragStartSub = dragStart$.subscribe((event: MouseEvent) => {
      ...

      dragSub = drag$.subscribe((event: MouseEvent) => {
        event.preventDefault();

        const x = event.clientX - initialX;
        const y = event.clientY - initialY;

        // 4 Update position relatively
        currentX = Math.max(minBoundX, Math.min(x, maxBoundX));
        currentY = Math.max(minBoundY, Math.min(y, maxBoundY));

        this.element.style.transform =
          "translate3d(" + currentX + "px, " + currentY + "px, 0)";
      });
    });

    const dragEndSub = dragEnd$.subscribe(() => {
      initialX = currentX;
      initialY = currentY;
      this.element.classList.remove("free-dragging");
      if (dragSub) {
        dragSub.unsubscribe();
      }
    });

    this.subscriptions.push.apply(this.subscriptions, [
      dragStartSub,
      dragSub,
      dragEndSub,
    ]);
  }
}

Sie müssen auch body einstellen 's Höhe auf 100 %, damit Sie das Element herumziehen können.

// src/styles.css

html,
body {
  height: 100%;
}

Sehen wir uns jetzt die Ausgabe an:

Das ist es! Hut ab... 🎉😀👍

Fazit

Lassen Sie uns kurz wiederholen, was wir getan haben:

✔️ Wir haben eine Direktive zum freien Ziehen erstellt

✔️ Dann wurde Unterstützung für den Ziehgriff hinzugefügt, damit der Benutzer andere Aktionen am Element ausführen kann

✔️ Zuletzt haben wir auch ein Grenzelement hinzugefügt, das dabei hilft, das zu ziehende Element innerhalb einer bestimmten Grenze zu halten

✔️ Und das alles ohne Bibliotheken von Drittanbietern 😉

Sie können noch viele weitere Funktionen hinzufügen, ich werde einige unten auflisten:

  1. Sperren von Achsen – erlaubt dem Benutzer nur in horizontaler oder vertikaler Richtung zu ziehen
  2. Ereignisse - Generieren Sie Ereignisse für jede Aktion, wie Ziehen-Start, Ziehen und Ziehen-Ende
  3. Position zurücksetzen - Bewegen Sie den Ziehpunkt in seine Ausgangsposition

Sie können diese Ziehfunktion in vielen Fällen verwenden, z. B. für ein schwebendes Widget, eine Chatbox, ein Hilfe- und Support-Widget usw. Sie können auch einen voll funktionsfähigen Editor erstellen, der Elemente (wie Kopfzeilen, Schaltflächen usw.) unterstützt herumgeschleppt.

Der gesamte obige Code ist auf Github verfügbar:

shhdharmen / angle-free-dragging

Erstellen Sie eine Anweisung in Angular, die es uns ermöglicht, jedes Element frei zu ziehen, ohne Bibliotheken von Drittanbietern zu verwenden.

Erstellen Sie eine Anweisung zum freien Ziehen in Angular

In diesem Artikel lernen wir, wie man eine Anweisung in Angular erstellt, die es uns ermöglicht, jedes Element frei zu ziehen, ohne Bibliotheken von Drittanbietern zu verwenden.

Lesen

Dieser Code wurde für meinen Artikel auf indepth.dev erstellt, Sie können ihn lesen unter:Direktive zum freien Ziehen in Angular erstellen.

Entwicklung

git clone https://github.com/shhdharmen/angular-free-dragging.git
cd angular-free-dragging
npm i
npm start
Auf GitHub ansehen

Vielen Dank für das Lesen dieses Artikels. Teilen Sie mir Ihre Gedanken und Ihr Feedback im Kommentarbereich mit.

Danksagungen

Beim Schreiben dieses Artikels habe ich Referenzen aus Codeschnipseln genommen, die bei w3schools und stackoverflow vorhanden sind.