RxJS:高级拖放 - 防止操作员订阅

RxJS: Advanced Drag and Drop - Prevent subscription in operator

我尝试在 RxJS 中实现拖放。我有一个 ID 为 draggable 的 DOM 节点,可以四处拖动。通过使用标准程序,拖放可以按预期工作。

但我试图增强拖放功能,这就是事情变得复杂的地方。我尝试在拖动开始后更改元素的背景颜色,并在它被放下后将其更改回来。

在我的方法中,我使用 switchMap 将鼠标移动事件的结果映射到由鼠标按下事件触发的可观察对象中。但是由于我使用鼠标弹起事件来完成 switchMaped 可观察对象(在下面的示例中为 mm$),所以我没有机会收到有关内部可观察对象的完成事件的通知,除非我正在订阅在 switchMap 运算符中添加它。

我知道在运算符内订阅远非好的做法,可能会导致内存泄漏。但是我还能做什么呢?如何才能做得更好?

Fiddle: https://jsfiddle.net/djwfyxs5/

const target = document.getElementById('draggable');
const mouseup$ = Observable.fromEvent(document, 'mouseup');
const mousedown$ = Observable.fromEvent(target, 'mousedown');
const mousemove$ = Observable.fromEvent(document, 'mousemove');

const move$ = mousedown$
  .switchMap(md => {
    md.target.style.backgroundColor = 'yellow';
    const {offsetX: startX, offsetY: startY} = md;
    const mm$ = mousemove$
      .map(mm => {
        mm.preventDefault();
        return {
          left: mm.clientX - startX,
          top: mm.clientY - startY
        };
      })
      .takeUntil(mouseup$);

    // Can the next line be avoided? 
    mm$.subscribe(null, null, () => {
      md.target.style.backgroundColor = 'purple';
    });

    return mm$;
  });

move$.subscribe((pos) => {
    target.style.top = pos.top + 'px';
    target.style.left = pos.left + 'px';
});

我在这里回答了一个类似的问题:

根据您的目的调整答案应该相当简单,因为流仍然包含公开事件的事件。

为了找到解决方案,我一直在努力解决这个问题。在我的尝试中,我使用了一个结合了 mousedownmouseup 事件的 helper observable。通过将它们与 combineLatest 运算符组合,可以访问 mousedown 事件的最新值,其中包含已单击的项目(目标)。

这让我可以在没有订阅的情况下正确设置颜色,如问题中所示。我的解决方案可以访问in this fiddle.

我不确定这是否可以完成 better/with 使用相同的想法减少代码。如果可能的话,我很乐意看到改进的实现。

完整代码:

const targets = document.getElementsByClassName('draggable');
const arrTargets = Array.prototype.slice.call(targets);

const mouseup$ = Rx.Observable.fromEvent(document, 'mouseup');
const mousedown$ = Rx.Observable.fromEvent(targets, 'mousedown');
const mousemove$ = Rx.Observable.fromEvent(document, 'mousemove');

// md      -------x------------------------------------------
// mm      xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// mu      -----------------------------------------x--------
// move$   -------xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx---------
// s$      -------x---------------------------------x--------

const s$ = Rx.Observable.combineLatest(
    mousedown$,
  mouseup$.startWith(null), // start immediately
    (md, mu) => {
    const { target } = md; // Target is always the one of mousedown event.
    let type = md.type;
    // Set type to event type of the newer event.
    if (mu && (mu.timeStamp > md.timeStamp)) {
        type = mu.type;
    }
    return { target, type };
    }
);

const move$ = mousedown$
  .switchMap(md => {
    const { offsetX: startX, offsetY: startY } = md;
    const mm$ = mousemove$
      .map(mm => {
        mm.preventDefault();
        return { 
          left: mm.clientX - startX,
          top: mm.clientY - startY,
          event: mm
        };
      })
      .takeUntil(mouseup$);
    return mm$;
  });

Rx.Observable.combineLatest(
    s$, move$.startWith(null), 
  (event, move) => {
    let newMove = move || {};
    // In case we have different targets for the `event` and
    // the `move.event` variables, the user switched the
    // items OR the user moved the mouse too fast so that the
    // event target is the document. 
    // In case the user switched to another element we want 
    // to ensure, that the initial position of the currently 
    // selected element is used.
    if (move && move.event.target !== event.target && arrTargets.indexOf(move.event.target) > -1) {
        const rect = event.target.getBoundingClientRect();
        newMove = {
        top: rect.top, // + document.body.scrollTop,
            left: rect.left // + document.body.scrollLeft
      };
    }
    return { event, move: newMove }
  }
)
  .subscribe(action => {
    const { event, move } = action;
    if (event.type === 'mouseup') {
        event.target.style.backgroundColor = 'purple';
      event.target.classList.remove('drag');
    } else {
        event.target.style.backgroundColor = 'red';
      event.target.classList.add('drag');
    }
    event.target.style.top = move.top + 'px';
    event.target.style.left = move.left + 'px';
  });