Как реализовать перетаскиваемый div в Angular 2 с помощью Rx

Я пытался заставить перетаскиваемый div работать с использованием Angular 2. Я использую этот пример из репозитория angular2-examples в качестве отправной точки, только корректируя код с учетом удаления метода toRx(). Код работает, но не учитывает mouseout событий. Это означает, что если я нажму на перетаскиваемый элемент div и медленно двигаю мышью, элемент div будет перемещаться вместе с мышью. Но если я двигаю мышь слишком быстро, вместо события mousemove отправляется событие mouseout, и перетаскивание останавливается.

Как я могу поддерживать перетаскивание после того, как мышь переместится так далеко, что будет запущено событие 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 {
}

person Chris    schedule 29.03.2016    source источник
comment
Возможный дубликат RxJs Как обрабатывать события документа   -  person Chris    schedule 29.03.2016


Ответы (4)


Я нашел ответ на этот вопрос в RxJs Как работать с событиями документа . Суть проблемы в том, что события мыши отправляются элементу только тогда, когда мышь находится над этим элементом. Итак, мы хотим, чтобы событие mousedown ограничивалось определенным элементом, но мы должны отслеживать глобальные события mousemove и mouseup. Вот новый код. Обратите внимание, что декоратор @HostListener используется для onMouseup, а onMousemove определяет цель как document:mouseup и document:mousemove. Вот как глобальные события передаются в поток Rx.

В официальной документации angular2 для HostListener это не упоминается. target:eventName, но этот старый дротик документация для 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';
            }
        });
    }
}
person Chris    schedule 29.03.2016
comment
есть проблема в конструкторе, может кто поправить? - person Julien; 04.04.2017
comment
Это викторина? - person Chris; 04.04.2017
comment
Можем ли мы ограничить область перетаскиваемого элемента? Например, элемент не должен выходить за пределы perticular div или что-то в этом роде? - person The Hungry Dictator; 20.04.2017
comment
Вызов getBoundingClientRect() 2 раза вместо введения локальной переменной? Какой неразумный способ написания кода - person EvAlex; 19.07.2017
comment
@EvAlex, getBoundingClientRect() - это функция-получатель из существующих свойств (интерфейса), ее двойной вызов не повлияет на производительность определенным образом. Кажется, у вас не так много опыта работы с общим javascript, пожалуйста, либо предложите, либо прокомментируйте в положительном ключе; воздержитесь от личных предпочтений без стандартов кодирования, чтобы поддержать свое субъективное понимание, особенно если вы не можете предложить решение самостоятельно. - person Gabriel Balsa Cantú; 21.03.2018
comment
проблема в том, что когда вы начнете добавлять эту директиву к большему количеству компонентов, производительность пострадает. @hostlistener, которые прослушивают документ, создаются несколько раз, что приводит к тому, что он срабатывает несколько раз, даже если он не имеет отношения к перетаскиваемому элементу. Mousemove ухудшает производительность, так как срабатывает при каждом экземпляре директивы и каждом перемещении пикселя. - person patelb; 05.05.2018
comment
@patelb у тебя есть лучший способ? Что бы вы сделали, чтобы решить эту проблему? - person Chris; 10.05.2018

Вы можете использовать это: 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','');
        }
}
person Ced    schedule 13.10.2016
comment
Вместо того, чтобы просто ссылаться на какой-то код, не могли бы вы объяснить различия? - person Chris; 14.10.2016
comment
@Chris, это похоже на ваш код, он не использует генератор событий и также обрабатывает сенсорные события для мобильных устройств. Поскольку я написал это, я решил поделиться этим здесь. Я добавил комментарий, хотя сейчас - person Ced; 14.10.2016
comment
@ Крис, не знаю, достаточно ли этого? Я также не стал устанавливать относительную позицию элемента при первом редактировании. Теперь все хорошо. - person Ced; 14.10.2016
comment
@Ced У вас есть рабочий образец? Демонстрационная ссылка — cedvdb.github.io/ng2draggable не работает. - person Sanket; 13.01.2017
comment
@Sanket К сожалению, нет. Однако я получил это cedvdb.github.io/virtual-pixels, где, если вы нажмете на рукой вы можете затем переместить куб в середине. Это была предыдущая версия, и в ней были ошибки, но суть вы поняли. В качестве альтернативы просто скопируйте код и попробуйте. Версия, которая находится в npm, не подходит, поэтому, если вы попробовали ее, у вас могли возникнуть проблемы. Я попробовал приведенный выше код прямо сейчас, и он работает. - person Ced; 13.01.2017
comment
Да, @Ced, твой код работает +1. По ошибке я использовал <div ng2-draggable> вместо <div [ng2-draggable]=true> - person Sanket; 13.01.2017
comment
@Sanket Да, я заметил, что мне нужно отредактировать файл readme на github. - person Ced; 13.01.2017
comment
отличный материал. возможно, нам придется настроить так, чтобы пользователи не могли перетаскивать панель, скрытую за окном браузера, поскольку у вас нет возможности снова захватить панель, чтобы перетащить ее;) - person Hung Bui; 22.05.2017
comment
Этот работает из коробки! Отличный материал @Ced, я использовал его в своем проекте Angular 5. - person Gabriel Balsa Cantú; 22.03.2018

У меня такая же проблема с перетаскиваемым всплывающим окном, поэтому я добавляю события mousemove и mouseup в документ при наведении мыши и удаляю их при наведении мыши. Я использую ответ Эрика Мартинеса для динамического добавления и удаления прослушивателя событий.

Шаблон:

<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.

person ali-myousefi    schedule 02.10.2017
comment
Мне потребовалась целая вечность, чтобы найти что-то хорошее, плункер отлично работает, и я смог встроить его в свой проект, спасибо! - person Jan Clemens Stoffregen; 23.02.2018
comment
Бесконечная загрузка... для меня на этом шлепке! - person Emerica; 23.08.2018
comment
Танк тебе @Curse. Это зависело от ссылки на ресурс Plunker. Я починил это. - person ali-myousefi; 25.08.2018
comment
Спасибо @Курс. Пожалуйста, попробуйте еще раз. - person ali-myousefi; 28.08.2018
comment
@ali-myousefi, похоже, работает с новой версией Plunker (url: next.plnkr. co/edit/AoaK7z?p=preview&preview), но не со старым ! - person Emerica; 28.08.2018

Вы можете создать большой div, который покрывает всю площадь экрана. Начнем с того, что этот div имеет меньший z-индекс, чем div, который вы хотите перетащить. При получении mousedown вы изменяете z-индекс div, чтобы он был выше, чем элемент перетаскивания, и получаете события перемещения мыши в этом div. Вы можете использовать это для вычисления положения элемента перетаскивания. Затем вы можете остановить и снова отправить div, когда получите мышь.

Недавно я написал модульную структуру перетаскивания в Angular2. Пожалуйста, попробуйте и оставьте отзыв.

https://github.com/ivegotwings/ng2Draggable

Однако я останавливаю перетаскивание после запуска события mouseout.

person shiv    schedule 29.03.2016
comment
.takeUntil(this._mouseout).takeUntil(this._mouseup); Это именно та проблема, которую я пытаюсь решить. Я не хочу останавливать перетаскивание mouseout событий. Пока кнопка мыши нажата, не имеет значения, куда пользователь перемещает мышь, перетаскивание должно продолжаться. - person Chris; 29.03.2016
comment
Привет, Крис! События перемещения мыши, которые используются для вычисления позиций, принимаются только до тех пор, пока вы находитесь в этом div. Я обновил свой ответ для работы. - person shiv; 29.03.2016