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

Posted

技术标签:

【中文标题】如何使用 Rx 在 Angular 2 中实现可拖动的 div【英文标题】:How to implement a draggable div in Angular 2 using Rx 【发布时间】:2016-07-16 09:19:02 【问题描述】:

我一直在尝试使用 Angular 2 让可拖动的 div 工作。我使用 this example from the angular2-examples repo 作为起点,只是真正调整代码以考虑删除 toRx() 方法。该代码有效,但它不考虑mouseout 事件。这意味着如果我点击一个 Draggable 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 How do deal with document events的可能重复 【参考方案1】:

我在RxJs How do deal with document events 中找到了答案。问题的症结在于鼠标事件仅在鼠标悬停在该元素上时才会发送到该元素。所以我们确实希望 mousedown 事件仅限于特定元素,但我们必须跟踪 global mousemovemouseup 事件。这是新代码。注意@HostListener 装饰器在onMouseuponMousemove 上的使用将目标指定为document:mouseupdocument:mousemove。这就是全局事件通过管道传输到 Rx 流中的方式。

official angular2 documentation for HostListener 没有提到这个 target:eventName 语法,但 this old dart documentation for 2.0.0-alpha.24 确实提到了它。它似乎在 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';
            
        );
    

【讨论】:

构造函数有问题,谁能指正? 这是一个测验吗? 我们可以限制可拖动元素的区域吗?像元素不应该移出特定的div之类的吗? 调用getBoundingClientRect() 2次而不是引入局部变量?多么不合理的代码编写方式 @EvAlex,getBoundingClientRect() 是来自现有属性(一个接口)的 getter 函数,调用它两次不会以特定方式影响性能。看来您对通用 javascript 没有太多经验,请建议或以积极的方式发表评论;在没有编码标准来支持您的主观洞察力的情况下,避免个人偏好,特别是如果您自己无法提供解决方案。【参考方案2】:

你可以使用这个: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 它与您的代码类似,它不使用事件发射器,它也处理手机的触摸事件。既然我写了它,我想我会在这里分享它。我现在添加了一些评论 @Chris 不知道够不够?第一次编辑时,我也省略了将元素位置放在相对位置上。现在好了。 @Ced 你有工作样本吗?演示链接 - cedvdb.github.io/ng2draggable 无效。 @Sanket 不幸的是没有。但是我得到了这个cedvdb.github.io/virtual-pixels,如果你点击手,你就可以在中间移动立方体。这是以前的版本,它有问题,但你明白了要点。或者,只需复制粘贴代码并尝试。 npm 上的版本不是正确的版本,所以如果你尝试了那个版本,你可能会遇到问题。我现在尝试了上面的代码,它可以工作【参考方案3】:

我对可拖动弹出窗口有同样的问题,所以我在 mousedown 时将 mousemove 和 mouseup 事件添加到文档中,并在 mouseup 时删除它们。 我使用Eric Martinez's answer 动态添加和删除事件监听器。

模板:

<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 工作得很好,我可以将它嵌入到我的项目中,谢谢! 无限“加载中...”为我在这个 plunk 上! 谢谢@Curse。这取决于Plunker resource link。我修好了。 谢谢@Curse。请再试一次。 @ali-myousefi 它看起来适用于新版本的 Plunker(网址:next.plnkr.co/edit/AoaK7z?p=preview&preview),但不适用于旧版本!【参考方案4】:

您可以创建一个覆盖屏幕空间的大 div。首先,此 div 的 z-index 低于您要拖动的 div。在接收 mousedown 时,您将 div 的 z-index 更改为高于拖动元素并在此 div 上接收鼠标移动事件。您可以 n 使用它来计算拖动元素的位置。然后,当您收到鼠标时,您可以停止并再次发送 div。

我最近在 Angular2 中编写了一个模块化的拖放框架。请试一试并提供反馈。

https://github.com/ivegotwings/ng2Draggable

但是,一旦触发 mouseout 事件,我就会停止拖动。

【讨论】:

.takeUntil(this._mouseout).takeUntil(this._mouseup); 这正是我要解决的问题。我不想停止对mouseout 事件的拖累。只要鼠标按钮按下,用户将鼠标移动到哪里都无关紧要,拖动应该继续。 嘿,克里斯,用于计算位置的鼠标移动事件只有在您位于该 div 时才会收到。我已经更新了我的解决方法。

以上是关于如何使用 Rx 在 Angular 2 中实现可拖动的 div的主要内容,如果未能解决你的问题,请参考以下文章

如何在pytorch中实现可微的汉明损失?

如何在 RealmRecyclerViewAdapter 中实现可过滤

如何在 Flutter 中实现可扩展面板?

如何在 C++ 中实现可配置位为 0 或 1 的消息类?

如何在 React/Express 应用程序中实现可选身份验证?

我们如何以编程方式在 Android Q OS 中实现可搜索的进度条?