timruffles / mobile-drag-drop

A drop-in shim to allow you to use existing html5 drag'n'drop code with mobile browsers
http://timruffles.github.io/mobile-drag-drop/demo/
MIT License
603 stars 151 forks source link

Adding ShadowDOM Support #115

Open z2oh opened 6 years ago

z2oh commented 6 years ago

I have started working on adding ShadowDOM support to this shim (which we are using for our Polymer 2 application).

At first, nothing was working at all. ShadowDOM introduces event retargeting, so events that fire up through Shadow Roots have their target changed to be the parent of the highest shadow root. This introduces a problem for the tryFindDraggableTarget function:

https://github.com/timruffles/ios-html5-drag-drop-shim/blob/0f2c6426d2618fadd1e9dfa4816fa0384b0291a0/src/index.ts#L188-L213

as event.target no longer refers to the actual target of the event. I rewrote this method (in JS) as follows:

    function tryFindDraggableTarget(event) {
        for(var i = 0; i < event.path.length; i++) {
            var el = event.path[i];
            if(el.draggable === false) {
                continue;
            }
            if (el.getAttribute && el.getAttribute("draggable") === "true") {
                return el;
            }
        }
    }

Success! I can now start drag operations successfully. Next up is dropping, which has proved to be more difficult. The code responsible for finding the drop target is here:

https://github.com/timruffles/ios-html5-drag-drop-shim/blob/0f2c6426d2618fadd1e9dfa4816fa0384b0291a0/src/index.ts#L722

document.elementFromPoint suffers a similar problem as before and returns only the highest shadow root rather than the actual element at the location. I found some documentation for DocumentOrShadowRoot.elementFromPoint(), but support for DocumentOrShadowRoot seems limited at best. This is where I got stuck. Does anyone have any ideas on how to move forward from here?

timruffles commented 6 years ago

Wow, thanks for this, would be very cool to support ShadowDOM 🤘 Haven't used ShadowDOM yet so unfortunately can only offer moral support.

reppners commented 6 years ago

Thanks for working on ShadowDOM support!

I'm currently rather of no help because of less free time for OSS and no detailed ShadowDOM knowledge either.

What I can offer though is a discussion about an API to make it easy to hook into those parts that need customization.

z2oh commented 6 years ago

That API seems like it will cover the bases, so that's definitely a good start.

I've stuck on how to implement userSelectionFromViewportCoordinates(x:number, y:number):Element with ShadowDOM all day. I'm worried we might have to wait for the browsers to catch up for this one.

I investigated trying to fire an event at an x,y coordinate and then use the same event.path method that I used earlier, but for security reasons the browser won't let you do this (otherwise one could write code to click buttons in an iframe for example). I got a hacky solution working in chrome by parsing the DOM down from the top up where the coordinates are, but that won't work in Safari (which is what I really need to support).

I'll keep trying to think of tricks, but it may be better to just wait until ShadowDOM support is more widespread (which hopefully won't be too much longer).

reppners commented 6 years ago

API additions released as of v2.3.0-rc.0

Let me know if the API's provided need tweaking.

krumware commented 6 years ago

thanks @reppners we'll check!

jogibear9988 commented 6 years ago

I've shadow dom support workin on my system.

This is my workin Code:

function tryFindDraggableTarget(event) {
    var cp = event.composedPath();
    for (let o of cp) {
        var el = o;
        do {
            if (el.draggable === false) {
                continue;
            }
            if (el.getAttribute && el.getAttribute("draggable") === "true") {
                return el;
            }
        } while ((el = el.parentNode) && el !== document.body);
    }
}

function elementFromPoint(x, y) {
    for (let o of this._path ) {
        if (o.elementFromPoint) {
            let el = o.elementFromPoint(x, y);
            if (el) {
                while (el.shadowRoot) {
                    el = el.shadowRoot.elementFromPoint(x, y);
                }
                return el;
            }
        }
    }
}

function dragStartConditionOverride(event) {
    this._path = event.composedPath();
    return true;
}
MobileDragDrop.polyfill({ tryFindDraggableTarget: tryFindDraggableTarget, elementFromPoint: elementFromPoint, dragStartConditionOverride: dragStartConditionOverride});
jogibear9988 commented 6 years ago

this works for me on my site on safari on ios

krumware commented 6 years ago

@z2oh can you check this out? lets bump this to the top of our (internal) priority list

reppners commented 6 years ago

@jogibear9988 Nice! Looks very elegant.

Please let me know if any utility functions may make sense for the polyfill to export so they can be reused in those custom implementations. The body of tryFindDraggableTarget seems to be reused as is, so that might make sense to export as a utility function, e.g. isElementDraggable().

I'm trying to wrap my head around why you're transporting the composedPath() from the initial drag start event to use it in your elementFromPoint(). Can you elaborate?

jogibear9988 commented 6 years ago

at first I tried to use hand over the lastEvent to the elementFromPoint, but when I call composedPath() there, I get an empty array!

seems I could only call it in the function with was raised from the event.

jogibear9988 commented 6 years ago

i've tested yesterday only on a iPad (where it worked). today I tested a galaxy tab and iPhone, both do not work. I will look whats different here

jogibear9988 commented 6 years ago

I've to correct myself. iPhone & galaxy tab work. I tested an old version

reppners commented 6 years ago

@jogibear9988 Thanks for the explanation!

The issue with your particular implementation is that elementFromPoint is invoked based on the last touchmove event. Coordinates will almost always be outside the boundaries of the element that started the drag operation, when composedPath is invoked and cached in _path.

If the whole application consists of custom elements it will work because composedPath will contain all the elements that have to be considered in elementFromPoint even when coordinates will be outside of the boundaries of the starting element.

But if only a few components are custom elements eventually not sharing a common root element than looping only through the elements that are part of composedPath when the drag operation starts might lead to silent failure.

Would this naive implementation for elementFromPoint work?

let el = document.elementFromPoint(x, y);
if (el) {
    // walk down custom component shadowRoots'
    while (el.shadowRoot) {
        let customEl = el.shadowRoot.elementFromPoint(x, y);
        // I'm a ShadowDom noob, can the element returned ever be the custom element itself?
        if(customEl === null || customEl === el) {
            break;
        }
        el = customEl;
    }
    return el;
}
reppners commented 6 years ago

Custom-element support should be a first class citizen of this polyfill but until browser support is solid and the implementation is battle-tested it makes sense to provide the needed custom implementations in a separate module similar as to how the scroll-behaviour module exports the function that enables automatic scrolling support when hovering at the edge of a scrollable container.

That being said I'm happy to accept PRs adding such a module to maintain and iterate on the implementation for custom-element support. Once browser support/the implementation has matured and is proven solid it can be added to the polyfill's default implementation.

jogibear9988 commented 6 years ago

@reppners I can only say, my code works in my application (with uses webcomponents everywhere!)

should I test your code, or what should I do?

reppners commented 6 years ago

If it's no trouble for you to test it - I'd be happy to know if it works this way, too.

Ultimately this repo needs a demo page that makes use of web components but I'm too short in time to work on this atm.

jogibear9988 commented 4 years ago

@reppners seems to work... sorry for the long delay :-(

jogibear9988 commented 4 years ago

would you include this? maybe create a setting to enable use of composedPath for shadowDom?

jogibear9988 commented 4 years ago

@reppners any news to this?

jogibear9988 commented 3 years ago

@timruffles could you merge this?

ghost commented 3 years ago

I'm facing the same issue when using web components. Neither this polyfill nor https://github.com/Bernardo-Castilho/dragdroptouch/issues/25 is working when using ShadowDOM.

@timruffles Would love to see this feature merged 😎

jogibear9988 commented 2 years ago

I used a little bit updates version of my polyfill in my project:

  // see https://github.com/timruffles/mobile-drag-drop/issues/115
  function tryFindDraggableTarget(event) {
    const cp = event.composedPath();
    for (const o of cp) {
        let el = o;
        do {
            if (el.draggable === false) {
                continue;
            }
            if (el.getAttribute && el.getAttribute('draggable') === 'true') {
                return el;
            }
        } while ((el = el.parentNode) && el !== document.body);
    }
  }

  function elementFromPoint(x, y) {
    let el = document.elementFromPoint(x, y);
    if (el) {
        // walk down custom component shadowRoots'
        while (el.shadowRoot) {
            let customEl = el.shadowRoot.elementFromPoint(x, y);
            // I'm a ShadowDom noob, can the element returned ever be the custom element itself?
            if (customEl === null || customEl === el) {
                break;
            }
            el = customEl;
        }
        return el;
    }
  }

  MobileDragDrop.polyfill({ tryFindDraggableTarget: tryFindDraggableTarget, elementFromPoint: elementFromPoint });

it works here: https://node-projects.github.io/web-component-designer-demo/index.html

what does not work is drag drop from jquery-fancytree, see issue here: https://github.com/mar10/fancytree/issues/1088

danziv commented 2 years ago

@timruffles can this fix be merged?

timruffles commented 2 years ago

Sorry I haven't been actively doing much frontend these days. I've not worked with the ShadowDOM APIs, for instance.

If someone makes a PR that everyone here is happy with I'd be happy to merge it.

jogibear9988 commented 2 years ago

https://github.com/timruffles/mobile-drag-drop/pull/168

danziv commented 2 years ago

If someone makes a PR that everyone here is happy with I'd be happy to merge it.

@timruffles i've tested @jogibear9988 's PR and it fixed the issue for me

reppners commented 2 years ago

@jogibear9988 @danziv @timruffles Took care of merging the PR and cutting a release as v3.0.0-beta.0.

danziv commented 2 years ago

thanks @reppners! checked the new version out - all working on my end, also in shadowDom.