如何使MatDialog可拖动 / Angular Material

55

是否可以使Angular Material对话框可拖动?我安装了angular2-draggable,并且当然可以在所有其他元素上使用该功能。

但是由于对话框是动态创建的,因此我无法将ngDraggable应用于特定元素或使用模板变量。


你知道如何使mat-dialog可拖动吗? - ark
不,目前似乎还没有解决方案。 也许随着即将到来的Angular Material 5会有所改善。 - HansDampfHH
似乎需要使用jQuery UI来实现这个..如果我错了请纠正我。 - ark
4个回答

117

自Angular Material 7以来的更新

你可以简单地使用来自@angular/cdk/drag-dropcdkDrag指令。

dialog.html

<h1 mat-dialog-title 
   cdkDrag
   cdkDragRootElement=".cdk-overlay-pane" 
   cdkDragHandle>
     Hi {{data.name}}
</h1>

Stackblitz 示例

之前的回答:

由于没有官方的解决方案,我将编写一个自定义指令,将其应用于对话框标题并为我们完成所有工作:

dialog.html

@Component({
  selector: 'app-simple-dialog',
  template: `
    <h1 mat-dialog-title mat-dialog-draggable-title>Hi {{data.name}}</h1>
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    <div mat-dialog-content>
      ...
    </div>
    <div mat-dialog-actions>
      ...
    </div>
  `
})
export class SimpleDialogComponent {

Ng-run 示例

图片描述

这里的基本思路是使用 MatDialogRef.updatePosition 方法更新对话框位置。在底层,该方法会更改 margin-top|margin-left 值,有人可能会认为这不是最佳选项,最好使用 transform,但我只是想展示一下我们如何利用内置服务而不使用某些技巧来完成它。

我们还需要在指令中注入 MatDialogContainer,以便可以获取对话框容器的初始位置。我们必须计算初始偏移量,因为 Angular Material 库使用 flex 来居中对话框,并且它不会给我们特定的 top/left 值。

dialog-draggable-title.directive.ts

import { Directive, HostListener, OnInit } from '@angular/core';
import { MatDialogContainer, MatDialogRef } from '@angular/material';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { takeUntil } from 'rxjs/operators/takeUntil';
import 'rxjs/add/observable/fromEvent';
import { take } from 'rxjs/operators/take';

@Directive({
  selector: '[mat-dialog-draggable-title]'
})
export class DialogDraggableTitleDirective implements OnInit {

  private _subscription: Subscription;

  mouseStart: Position;

  mouseDelta: Position;

  offset: Position;

  constructor(
    private matDialogRef: MatDialogRef<any>,
    private container: MatDialogContainer) {}

  ngOnInit() {
    this.offset = this._getOffset();
  }

  @HostListener('mousedown', ['$event'])
  onMouseDown(event: MouseEvent) {
    this.mouseStart = {x: event.pageX, y: event.pageY};

    const mouseup$ = Observable.fromEvent(document, 'mouseup');
    this._subscription = mouseup$.subscribe(() => this.onMouseup());

    const mousemove$ = Observable.fromEvent(document, 'mousemove')
      .pipe(takeUntil(mouseup$))
      .subscribe((e: MouseEvent) => this.onMouseMove(e));

    this._subscription.add(mousemove$);
  }

  onMouseMove(event: MouseEvent) {
      this.mouseDelta = {x: (event.pageX - this.mouseStart.x), y: (event.pageY - this.mouseStart.y)};

      this._updatePosition(this.offset.y + this.mouseDelta.y, this.offset.x + this.mouseDelta.x);
  }

  onMouseup() {
    if (this._subscription) {
      this._subscription.unsubscribe();
      this._subscription = undefined;
    }

    if (this.mouseDelta) {
      this.offset.x += this.mouseDelta.x;
      this.offset.y += this.mouseDelta.y;
    }
  }

  private _updatePosition(top: number, left: number) {
    this.matDialogRef.updatePosition({
      top: top + 'px',
      left: left + 'px'
    });
  }

  private _getOffset(): Position {
    const box = this.container['_elementRef'].nativeElement.getBoundingClientRect();
    return {
      x: box.left + pageXOffset,
      y: box.top + pageYOffset
    };
  }
}


export interface Position {
  x: number;
  y: number;
}

记住位置

由于@Rolando的要求:

我希望“记住”模态框的位置,这样当单击打开模态框的按钮时,模态框会在“上次定位”的位置打开。

我们尝试支持它。

为此,您可以创建一个服务来存储对话框位置:

modal-position.cache.ts

@Injectable()
export class ModalPositionCache {
  private _cache = new Map<Type<any>, Position>();

  set(dialog: Type<any>, position: Position) {
    this._cache.set(dialog, position);
  }

  get(dialog: Type<any>): Position|null {
    return this._cache.get(dialog);
  }
}

现在你需要将此服务注入到我们的指令中:

dialog-draggable-title.directive.ts

export class DialogDraggableTitleDirective implements OnInit {
  ...

  constructor(
    private matDialogRef: MatDialogRef<any>,
    private container: MatDialogContainer,
    private positionCache: ModalPositionCache
  ) {}

  ngOnInit() {
    const dialogType = this.matDialogRef.componentInstance.constructor;
    const cachedValue = this.positionCache.get(dialogType);
    this.offset = cachedValue || this._getOffset();
    this._updatePosition(this.offset.y, this.offset.x);

    this.matDialogRef.beforeClose().pipe(take(1))
      .subscribe(() => this.positionCache.set(dialogType, this.offset));
  }

当对话框即将关闭时,我会保存最后的偏移量。

Ng-run示例

这样对话框就记住了它关闭时的位置。

输入图像描述


我不得不在 ngAfterViewInit() 中设置偏移量,而不是 ngOnInit() - 现在它工作正常! - TmTron
1
我已经尝试了您更新的Angular 9解决方案,但整个对话框都像拖动手柄一样,而不仅仅是标题。有什么想法吗? - Xander
很抱歉,我不能这样做。我使用了Material 9 Stackblitz示例中的对话框,并添加了这3个指令,它可以正常工作。所以这可能是我的应用程序特定的问题,但我不知道是什么原因...我已经到了无法再思考的地步。 - Xander
2
请注意,在某些情况下,似乎需要将cdkDrag和cdkDragRootElement移动到cdkDragHandle的父元素中,以防止整个对话框可拖动。 - JW.
我建议在<h1>标签中添加style="cursor: move",这样用户就能理解该元素是可拖动的。 - yglodt
显示剩余6条评论

18

在你的模块中,导入CDK Drag模块。

import { DragDropModule } from '@angular/cdk/drag-drop';

在HTML中,例如对话框,只需添加到任何HTML元素。我已经添加到第一个元素,然后我可以将对话框拖动到任何我选择的位置。

<mat-dialog-content cdkDrag cdkDragRootElement=".cdk-overlay-pane" cdkDragHandle>
  content...
</mat-dialog-content>

当我使用this.dialog.open打开对话框时,您知道如何操作吗?我的HTML中没有mat-dialog-content,它由后台管理。 - Abhishek Mittal
你能否在Stackblitz或其他地方发布一个示例? 当您使用this.dialog.open时,您需要指定要打开的组件,在该组件中添加这些指令,并且将其作为示例发布。 - pinarella
这是一个示例:https://stackblitz.com/edit/angular-jtnajz?file=src%2Fapp%2Fdialog-animations-example-dialog.html - pinarella
使用dialog.open API方法添加可拖动效果的最简单方式 - undefined

1

0
angular2-draggable中,您可以使用ngDraggable使元素可拖动。 其中ngDraggable是一个指令,在您的情况下,您需要将此ngDraggable指令动态地附加到您动态创建的对话框上。
尽管官方上没有办法动态添加指令,但以下问题中已经讨论了一些不太正规的技巧来动态添加指令。 如何动态添加指令? 在另一个指令的主机中使用Angular2指令

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接