如何在Angular 2中使用Rx实现可拖动的div

22
我一直在尝试使用 Angular 2 来实现可拖动的 div。我使用 angular2-examples 仓库中的示例 作为起点,只是稍微调整了代码以适应 toRx() 方法的删除。代码能够工作,但它没有考虑到 mouseout 事件。这意味着,如果我点击一个可拖动的 div 并缓慢移动鼠标,div 将随着鼠标移动。但是如果我移动鼠标太快,则会发送 mouseout 事件而不是 mousemove 事件,导致拖动停止。
如何在发生 mouseout 事件后继续进行拖动?我尝试将 mouseout 事件流与 mousemove 事件流合并,以便将 mouseout 事件视为与 mousemove 相同,但这种方法不起作用。
我正在使用 Angular 2.0.0-beta.12。
import {Component, Directive, HostListener, EventEmitter, ElementRef, OnInit} from 'angular2/core';
import {map, merge} from 'rxjs/Rx';

@Directive({
    selector: '[draggable]'
})
export class Draggable implements OnInit {

    mouseup = new EventEmitter();
    mousedown = new EventEmitter();
    mousemove = new EventEmitter();
    mouseout = new EventEmitter();

    @HostListener('mouseup', ['$event'])
    onMouseup(event) {
        this.mouseup.emit(event);
    }

    @HostListener('mousedown', ['$event'])
    onMousedown(event) {
        this.mousedown.emit(event);
        return false; // Call preventDefault() on the event
    }

    @HostListener('mousemove', ['$event'])
    onMousemove(event) {
        this.mousemove.emit(event);
    }

    @HostListener('mouseout', ['$event'])
    onMouseout(event) {
        this.mouseout.emit(event);
        return false; // Call preventDefault() on the event
    }

    constructor(public element: ElementRef) {
        this.element.nativeElement.style.position = 'relative';
        this.element.nativeElement.style.cursor = 'pointer';

        map;
        merge;
        this.mousedrag = this.mousedown.map(event => {
            return {
                top: event.clientY - this.element.nativeElement.getBoundingClientRect().top
                left: event.clientX - this.element.nativeElement.getBoundingClientRect().left,
            };
        })
        .flatMap(
            imageOffset => this.mousemove.merge(this.mouseout).map(pos => ({
                top: pos.clientY - imageOffset.top,
                left: pos.clientX - imageOffset.left
            }))
            .takeUntil(this.mouseup)
        );
    }

    ngOnInit() {
        this.mousedrag.subscribe({
            next: pos => {
                this.element.nativeElement.style.top = pos.top + 'px';
                this.element.nativeElement.style.left = pos.left + 'px';
            }
        });
    }
}

@Component({
    selector: 'my-app',
    template: `
        <div draggable>
            <h1>Hello, World!</h1>
        </div>
        `,
    directives: [Draggable,],
})
export class AppComponent {
}

可能是RxJs如何处理文档事件的重复问题。 - Chris
5个回答

30

我在RxJs 如何处理文档事件中找到了这个答案。问题的关键在于当鼠标位于特定元素上时,才会将鼠标事件发送到该元素。因此,我们确实希望将mousedown事件限制在特定元素上,但我们必须跟踪全局的mousemovemouseup事件。下面是新代码。请注意,在onMouseuponMousemove上使用@HostListener装饰器指定目标为document:mouseupdocument:mousemove。这就是如何将全局事件传送到Rx流中。

官方的 Angular2 HostListener 文档没有提到这种target:eventName语法,但是2.0.0-alpha.24旧Dart文档中提到了它。它似乎仍然适用于2.0.0-beta.12。

@Directive({
    selector: '[draggable]'
})
export class Draggable implements OnInit {

    mouseup = new EventEmitter<MouseEvent>();
    mousedown = new EventEmitter<MouseEvent>();
    mousemove = new EventEmitter<MouseEvent>();

    mousedrag: Observable<{top, left}>;

    @HostListener('document:mouseup', ['$event'])
    onMouseup(event: MouseEvent) {
        this.mouseup.emit(event);
    }

    @HostListener('mousedown', ['$event'])
    onMousedown(event: MouseEvent) {
        this.mousedown.emit(event);
        return false; // Call preventDefault() on the event
    }

    @HostListener('document:mousemove', ['$event'])
    onMousemove(event: MouseEvent) {
        this.mousemove.emit(event);
    }

    constructor(public element: ElementRef) {
        this.element.nativeElement.style.position = 'relative';
        this.element.nativeElement.style.cursor = 'pointer';

        this.mousedrag = this.mousedown.map(event => {
            return {
                top: event.clientY - this.element.nativeElement.getBoundingClientRect().top
                left: event.clientX - this.element.nativeElement.getBoundingClientRect().left,
            };
        })
        .flatMap(
            imageOffset => this.mousemove.map(pos => ({
                top: pos.clientY - imageOffset.top,
                left: pos.clientX - imageOffset.left
            }))
            .takeUntil(this.mouseup)
        );
    }

    ngOnInit() {
        this.mousedrag.subscribe({
            next: pos => {
                this.element.nativeElement.style.top = pos.top + 'px';
                this.element.nativeElement.style.left = pos.left + 'px';
            }
        });
    }
}

2
构造函数出现问题,有人可以纠正吗? - Julien
10
这是一道测验题吗? - Chris
我们能限制可拖动元素的区域吗?比如说,该元素不应该移出特定的div或其他什么地方吗? - The Hungry Dictator
调用getBoundingClientRect()两次而不是引入本地变量?这是一种不合理的编写代码的方式。 - EvAlex
4
@EvAlex,getBoundingClientRect()是一个获取现有属性(接口)的getter函数,调用两次不会以任何特定方式影响性能。 看起来你对一般的JavaScript经验不太丰富,请提出建议或以积极的方式评论;请避免没有编码标准支持的主观见解和个人偏好,特别是如果你自己无法提供解决方案。 - Gabriel Balsa Cantú
显示剩余2条评论

6
你可以使用以下命令安装: npm install ng2draggable 使用[ng2-draggable]="true",别忘了="true" 你可以在这里找到它:

https://github.com/cedvdb/ng2draggable

以下是代码:
@Directive({
  selector: '[ng2-draggable]'
})
export class Draggable implements OnInit{
    topStart:number=0;
    leftStart:number=0;
    _allowDrag:boolean = true;
    md:boolean;

    constructor(public element: ElementRef) {}

        ngOnInit(){
          // css changes
          if(this._allowDrag){
            this.element.nativeElement.style.position = 'relative';
            this.element.nativeElement.className += ' cursor-draggable';
          }
        }

        @HostListener('mousedown', ['$event'])
        onMouseDown(event:MouseEvent) {
          if(event.button === 2)
            return; // prevents right click drag, remove his if you don't want it
          this.md = true;
          this.topStart = event.clientY - this.element.nativeElement.style.top.replace('px','');
          this.leftStart = event.clientX - this.element.nativeElement.style.left.replace('px','');
        }

        @HostListener('document:mouseup')
        onMouseUp(event:MouseEvent) {
          this.md = false;
        }

        @HostListener('document:mousemove', ['$event'])
        onMouseMove(event:MouseEvent) {
          if(this.md && this._allowDrag){
            this.element.nativeElement.style.top = (event.clientY - this.topStart) + 'px';
            this.element.nativeElement.style.left = (event.clientX - this.leftStart) + 'px';
          }
        }

        @HostListener('touchstart', ['$event'])
        onTouchStart(event:TouchEvent) {
          this.md = true;
          this.topStart = event.changedTouches[0].clientY - this.element.nativeElement.style.top.replace('px','');
          this.leftStart = event.changedTouches[0].clientX - this.element.nativeElement.style.left.replace('px','');
          event.stopPropagation();
        }

        @HostListener('document:touchend')
        onTouchEnd() {
          this.md = false;
        }

        @HostListener('document:touchmove', ['$event'])
        onTouchMove(event:TouchEvent) {
          if(this.md && this._allowDrag){
            this.element.nativeElement.style.top = ( event.changedTouches[0].clientY - this.topStart ) + 'px';
            this.element.nativeElement.style.left = ( event.changedTouches[0].clientX - this.leftStart ) + 'px';
          }
          event.stopPropagation();
        }

        @Input('ng2-draggable')
        set allowDrag(value:boolean){
          this._allowDrag = value;
          if(this._allowDrag)
            this.element.nativeElement.className += ' cursor-draggable';
          else
            this.element.nativeElement.className = this.element.nativeElement.className
                                                    .replace(' cursor-draggable','');
        }
}

请不要只是简单地链接到一些代码,能否请您解释一下它们之间的区别? - Chris
2
@Chris,这段代码与你的代码类似,但它不使用事件发射器,并且还处理移动设备的触摸事件。既然我写了它,我想在这里分享一下。我现在添加了一些注释。 - Ced
@Chris 不知道这是否足够?我还忘了在第一次编辑时将元素位置设置为相对。现在没问题了。 - Ced
很棒的东西。我们可能需要调整一下,以便用户无法拖动隐藏在浏览器窗口后面的工具栏,因为您再也没有办法抓住该工具栏并将其拖动了 ;) - Hung Bui
这个可以直接使用!太棒了@Ced,我在我的Angular 5项目中使用了它。 - Gabriel Balsa Cantú
显示剩余4条评论

2
我有一个与可拖动弹出窗口相关的问题,所以我在mousedown事件上添加了mousemove和mouseup事件,并在mouseup上将它们删除。 我使用Eric Martinez的答案来动态添加和删除事件监听器。
模板:
<div class="popup-win" (mousedown)="mousedown($event)"></div>

组件:
constructor(private elementRef: ElementRef,
        private renderer: Renderer2) {}

mousedown(event: any) {
    this.xStartElementPoint = this.curX;
    this.yStartElementPoint = this.curY;
    this.xStartMousePoint = event.pageX;
    this.yStartMousePoint = event.pageY;
    this.mousemoveEvent = this.renderer.listen("document", "mousemove", this.dragging);
    this.mouseupEvent = this.renderer.listen("document", "mouseup", this.mouseup);
}

dragging(event: any) {
     this.curX = this.xStartElementPoint + (event.pageX - this.xStartMousePoint);
     this.curY = this.yStartElementPoint + (event.pageY - this.yStartMousePoint);
}
mouseup(event: any) {
    // Remove listeners
    this.mousemoveEvent();
    this.mouseupEvent();
}

这是一个可以运行的示例,链接在Plunker上。

找好东西花了我很长时间,这个 Plunker 很好用,我可以将其嵌入我的项目中,谢谢! - Jan Clemens Stoffregen
这个 Plunk 对我来说一直是无限的“加载中…”! - Emeric
谢谢@Curse。这取决于Plunker资源链接。我已经修复了它。 - ali-myousefi
谢谢@Curse。请再试一次。 - ali-myousefi
@ali-myousefi,它似乎可以在新版本的Plunker(网址:https://next.plnkr.co/edit/AoaK7z?p=preview&preview)中工作,但在旧版本中不行! - Emeric

1
你可以创建一个覆盖整个屏幕的大div。首先,这个div的z-index比你想拖动的div低。在接收到mousedown事件时,您将该div的z-index更改为高于拖动元素,并在此div上接收鼠标移动事件。然后,您可以使用它来计算拖动元素的位置。当您接收到mouseup事件时,可以停止并将div发送回去。
我最近在Angular2中编写了一个模块化的拖放框架。请试用并提供反馈。

https://github.com/ivegotwings/ng2Draggable

然而,一旦触发mouseout事件,我就停止拖动。

.takeUntil(this._mouseout).takeUntil(this._mouseup); 这正是我要解决的问题。我不想在 mouseout 事件上停止拖动。只要鼠标按钮按下,无论用户将鼠标移动到哪里,拖动都应该继续。 - Chris
嗨,Chris,用于计算位置的鼠标移动事件只有在您停留在该div上时才会收到。我已经更新了我的答案以解决这个问题。 - shiv

0
@ali-myousefi提出的解决方案很有前途,但由于多种原因而无法工作。我花了几个小时尝试让它工作,这是结果:
模板:
<div (mousedown)="mousedown($event)">a draggable block</div>
[…]
<div (mousedown)="mousedown($event)">another draggable block</div>

组件:

import { Component, Renderer2 } from '@angular/core';

@Component({
  selector: 'app-foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foorbar.component.scss'],
})
export class FoobarComponent {

  mousemoveEvent: any;
  mouseupEvent: any;

  movedElement: any;

  curX: number;
  curY: number;
  offsetX = 0;
  offsetY = 0;
  xStartElementPoint: number;
  yStartElementPoint: number;
  xStartMousePoint: number;
  yStartMousePoint: number;

  constructor(private renderer: Renderer2) {}

  mousedown(event: MouseEvent) {
    this.movedElement = this.renderer.selectRootElement(event.target, true);
    this.setInitialAbsolutePos(this.movedElement);
    this.xStartElementPoint = this.curX;
    this.yStartElementPoint = this.curY;
    this.xStartMousePoint = event.pageX;
    this.yStartMousePoint = event.pageY;
    this.mousemoveEvent = this.renderer.listen("document", "mousemove", this.dragging.bind(this));
    this.mouseupEvent = this.renderer.listen("document", "mouseup", this.mouseup.bind(this));

    return false; // Call preventDefault() on the event
}

  dragging(event: any) {
    this.curX = this.xStartElementPoint + (event.pageX - this.xStartMousePoint + this.offsetX);
    this.curY = this.yStartElementPoint + (event.pageY - this.yStartMousePoint + this.offsetY);
    this.moveElement(this.movedElement, this.curX, this.curY);

    return false; // Call preventDefault() on the event
  }

  mouseup(event: any) {
    // Remove listeners
    this.mousemoveEvent();
    this.mouseupEvent();

    return false; // Call preventDefault() on the event
  }

  moveElement(element, curX, curY) {
    // update the position of the div:
    this.renderer.setStyle(element, 'left', curX + 'px');
    this.renderer.setStyle(element, 'top', curY + 'px');
    this.renderer.setStyle(element, 'right', 'initial'); // required in case the element was previously right-aligned...
    this.renderer.setStyle(element, 'bottom', 'initial'); // required in case the element was previously bottom-aligned...
  }

  setInitialAbsolutePos(element: any) {
    this.curX = element.getBoundingClientRect().left;
    this.curY = element.getBoundingClientRect().top;

    // set position:absolute (if not already done)
    this.renderer.setStyle(element, 'position', 'absolute');

    // compensate for the new position:absolute
    // and/or padding / margin / borders (if present)
    // by making a move of 0 pixels and then compute the offset:
    this.moveElement(element, this.curX, this.curY);
    const afterX = element.getBoundingClientRect().left;
    const afterY = element.getBoundingClientRect().top;
    this.offsetX = (this.curX - afterX);
    this.offsetY = (this.curY - afterY);
    if (this.offsetX != 0 || this.offsetY != 0) {
      this.moveElement(element, this.curX + this.offsetX, this.curY + this.offsetY);
    }
  }

}

优点:

  • 即使存在边距、填充、边框等,也可以正常工作。
  • 即使元素以前没有设置为position:absolute,也可以正常工作。
  • 一个单一的代码可以用于使多个不同的元素可拖动。
  • 不需要在每个元素上放置id=”“。
  • 无需添加额外的库。

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