Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding ShadowDOM Support #115

Open
z2oh opened this issue Nov 9, 2017 · 28 comments
Open

Adding ShadowDOM Support #115

z2oh opened this issue Nov 9, 2017 · 28 comments

Comments

@z2oh
Copy link

z2oh commented Nov 9, 2017

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
Copy link
Owner

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

@reppners
Copy link
Collaborator

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
Copy link
Author

z2oh commented Nov 13, 2017

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
Copy link
Collaborator

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

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

@krumware
Copy link

thanks @reppners we'll check!

@jogibear9988
Copy link
Contributor

jogibear9988 commented Mar 22, 2018

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
Copy link
Contributor

this works for me on my site on safari on ios

@krumware
Copy link

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

@reppners
Copy link
Collaborator

reppners commented Mar 23, 2018

@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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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

@reppners
Copy link
Collaborator

reppners commented Mar 25, 2018

@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
Copy link
Collaborator

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
Copy link
Contributor

@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
Copy link
Collaborator

reppners commented Apr 1, 2018

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
Copy link
Contributor

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

@jogibear9988
Copy link
Contributor

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

@jogibear9988
Copy link
Contributor

@reppners any news to this?

@jogibear9988
Copy link
Contributor

@timruffles could you merge this?

@ghost
Copy link

ghost commented Oct 17, 2020

I'm facing the same issue when using web components.
Neither this polyfill nor drag-drop-touch-js/dragdroptouch#25 is working when using ShadowDOM.

@timruffles Would love to see this feature merged 😎

@jogibear9988
Copy link
Contributor

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:
mar10/fancytree#1088

@danziv
Copy link

danziv commented Jun 20, 2022

@timruffles can this fix be merged?

@timruffles
Copy link
Owner

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
Copy link
Contributor

#168

@danziv
Copy link

danziv commented Jun 22, 2022

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
Copy link
Collaborator

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

@danziv
Copy link

danziv commented Jun 26, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants