Search code examples
javascriptecmascript-6es6-promise

Is there a way to cleanly create a promise that resolves after left mouse up or down?


In my client code, I have

const whatMouseDid = await mouseDoesSomething()

with the latter function looking like:

async mouseDoesSomething() {
    const mouseUp = (resolve) => {
        const handler = (evt) => {
            if (evt.button === 0)
                resolve("up")
        }
        return handler
    }
    const mouseDown = (resolve) => {
        const handler = (evt) => {
            if (evt.button === 0)
                resolve("down")
        }
        return handler
    }
    return new Promise((resolve, reject) => {
        document.addEventListener('mouseup', mouseUp(resolve))
        document.addEventListener('mousedown', mouseDown(resolve))
    })
}

Which is already a bit more convoluted than I'd prefer, but still manageable. However, there's a problem - the listeners are never removed. And because I need to pass in a reference to Promise.resolve, I can't removeEventListener easily. The only way I can think to do this is to keep a mutable list of handlers and the events, targets, etc they are assigned to, and in the handler function(s), iterate over that list and remove attached handlers. The optional param {once: true} also won't work because I don't resolve if the button clicked is not the one I want.

This all feels super convoluted, and makes me think I'm just missing the obvious easy way to do this; am I? Or is it really this much of nuisance?


Solution

  • And because I need to pass in a reference to Promise.resolve, I can't removeEventListener easily. The only way I can think to do this is to keep a mutable list

    Why so complicated? You create functions in the promise executor, just reference them.

    mouseDoesSomething() {
        return new Promise((resolve, reject) => {
            const mouseUphandler = evt => {
                if (evt.button === 0) {
                    resolve("up")
                    document.removeEventListener('mouseup', mouseUpHandler)
                    document.removeEventListener('mousedown', mouseDownHandler)
                }
            }
            const mouseDownhandler = evt => {
                if (evt.button === 0) {
                    resolve("down")
                    document.removeEventListener('mouseup', mouseUpHandler)
                    document.removeEventListener('mousedown', mouseDownHandler)
                }
            }
            document.addEventListener('mouseup', mouseUpHandler)
            document.addEventListener('mousedown', mouseDownHandler)
        })
    }
    

    Surely that's lots of code duplication, but you can abstract this again by creating the handlers dynamically - you just need to do it in the scope where the two handlers are declared:

    mouseDoesSomething() {
        return new Promise((resolve, reject) => {
            const makeHandler = dir => evt => {
                if (evt.button === 0) {
                    resolve(dir)
                    document.removeEventListener('mouseup', mouseUpHandler)
                    document.removeEventListener('mousedown', mouseDownHandler)
                }
            }
            const mouseUphandler = makeHandler("up")
            const mouseDownhandler = makeHandler("down")
            document.addEventListener('mouseup', mouseUpHandler)
            document.addEventListener('mousedown', mouseDownHandler)
        })
    }