how do I get xstream to combine mouse events?

164 Views Asked by At

I'm experimenting with drag-and-drop using cyclejs in a codepen. The standard drag methods supported by HTML 5 don't seem to support constraints on the movement of the dragged object so I went with standard mousedown/mousemove/mouseup. It works, but not consistently. The combine() operation doesn't seem to trigger even when the debug() calls show that mousedown and mousemove events have been received and sometimes the mouseup is missed. Perhaps my understanding of the operation is incomplete or incorrect. A direct link to the codepen is provided at the bottom of this post. Any help appreciated!

const xs = xstream.default;
const { run } = Cycle;
const { div, svg, makeDOMDriver } = CycleDOM;

function DragBox(sources) {
  const COMPONENT_NAME = `DragBox`;

  const intent = function({ DOM }) {
    return {
      mousedown$: DOM.select(`#${COMPONENT_NAME}`)
        .events("mousedown")
        .map(function(ev) {
          return ev;
        })
        .debug("mousedown"),
      mousemove$: DOM.select(`#${COMPONENT_NAME}`)
        .events("mousemove")
        .map(function(ev) {
          return ev;
        })
        .debug("mousemove"),
      mouseup$: DOM.select("#container")
        .events("mouseup")
        .map(function(ev) {
          return ev;
        })
        .debug("mouseup")
    };
  };

  const between = (first, second) => {
    return source => first.mapTo(source.endWhen(second)).flatten();
  };

  const model = function({ mousedown$, mousemove$, mouseup$ }) {
    return xs
      .combine(mousedown$, mousemove$)
      .debug("combine")
      .map(([mousedown, mousemove]) => ({
        x: mousemove.pageX - mousedown.layerX,
        y: mousemove.pageY - mousedown.layerY
      }))
      .compose(between(mousedown$, mouseup$))
      .startWith({
        x: 0,
        y: 0
      })
      .debug("model");
  };

  const getStyle = left => top => {
    return {
      style: {
        position: "absolute",
        left: left + "px",
        top: top + "px",
        backgroundColor: "#333",
        cursor: "move"
      }
    };
  };

  const view = function(state$) {
    return state$.map(value =>
      div("#container", { style: { height: "100vh" } }, [
        div(`#${COMPONENT_NAME}`, getStyle(value.x)(value.y), "Move Me!")
      ])
    );
  };

  const actions = intent(sources);
  const state$ = model(actions);
  const vTree$ = view(state$);

  return {
    DOM: vTree$
  };
}

function main(sources) {
  const dragBox = DragBox(sources);

  const sinks = {
    DOM: dragBox.DOM
  };

  return sinks;
}

Cycle.run(main, {
  DOM: makeDOMDriver("#app")
});

https://codepen.io/velociflapter/pen/bvqMGp?editors=1111

1

There are 1 best solutions below

3
On BEST ANSWER

Testing your code shows that combine is not getting the first mousedown event, apparently due to the between operator subscribing to mousedown$ after the first mousedown event. Adding remember to the mousedown$ sends that first mousedown event to the between operator subscription.

mousedown$: DOM.select(`#${COMPONENT_NAME}`)
  .events("mousedown").remember()

Codepen.io remember example.

Codesandbox.io testing between


Here's another CycleJS/xstream Drag and Drop approach (taking inspiration from this RxJS Drag and Drop example) I think is more straight forward. Everything else in your code is essentially the same but the model function is this:

const model = function({ mousedown$, mousemove$, mouseup$ }) {
  return mousedown$.map(md => {
    let startX = md.offsetX, startY = md.offsetY
    return mousemove$.map(mm => {
      mm.preventDefault()
      return {
        x: mm.clientX - startX,
        y: mm.clientY - startY
      }
    }).endWhen(mouseup$)
  }).flatten().startWith({x:0,y:0})
};

Here's a Codepen.io example.