-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Add shadow dom support #1805
base: develop
Are you sure you want to change the base?
Add shadow dom support #1805
Conversation
core/emitter.js
Outdated
@@ -4,15 +4,13 @@ import logger from './logger'; | |||
let debug = logger('quill:events'); | |||
|
|||
const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click']; | |||
const EMITTERS = new Set(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we use a weakmap for this? not sure what the value would be though..
core/selection.js
Outdated
this.emitter.listenDOM('mousedown', document.body, () => { | ||
const node = (this.rootDocument === document ? document.body : this.rootNode); | ||
|
||
this.emitter.listenDOM('mousedown', node, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should these be reverted to document
? why do we listen on document for mouse events? shouldn't we only listen on the editor container?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to know if you drag outside of the boundaries of the editor container. See the lines below this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah fair enough, this can probably be reverted then as its specific to the body rather than the container.
21a05e9
to
1927ef3
Compare
Some general thoughts: I would like to see some tests to guide this development. There are lots of ways to implement this but there's no point in debating the best way without a starting point of what even works. Second we cannot use any features IE11 does not support (like Set) which is why the emitter was implemented the way it is. |
ah fair enough, it can be changed to an array. as for tests, the "WIP" part of this PR was that i need to add tests, don't worry 👍 so far i have been testing by placing the container for all tests in a shadow root. to ensure all tests which pass outside of one, pass inside of one too. i will add some tests soon as i can, though. i did find that a couple of tests fail in a shadow root because a |
@jhchen something weird definitely happens with those br tags... in chrome, if i make a simple element:
The outer |
any update on this? any help needed? |
sorry for the slight delay, haven't been around recently. i'll check this out tonight and see if i can get the tests added. current failures seem to be due to the size of a line break differing in shadow DOM. |
@jhchen i just ran the full test suite inside a shadow DOM container and all tests pass in chrome now (css was not being loaded the first time i tried so So at least this means now the aim of this PR has been met. I'll try put some work in on writing additional tests next |
core/emitter.js
Outdated
|
||
EVENTS.forEach(function(eventName) { | ||
document.addEventListener(eventName, (...args) => { | ||
[].slice.call(document.querySelectorAll('.ql-container')).forEach((node) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was added as a memory leak way to add event listeners on global objects. If WeakMap were available it would be used like the comment mentions but since it is not it does this which by the way is how WeakMaps work under the hook.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old method won't work with shadow DOM due to the fact that querySelectorAll
will not penetrate through shadow roots.
Though I can see how EMITTERS
could keep growing if you repeatedly make new instances (and they never be released). Needs some thought..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why couldn't emitters be passed as an initialization argument? Making this global is a recipe for leaks and also makes encapsulating the events for ShadowDOM very hard. It seems we might be able to make emitter into an interface, then have a GlobalEventEmitter
and a ShadownDOMEventEmitter
where Global does the current global things and the ShadowDOMVersion is initialized with the ShadowRoot or editor root.
I'd be down to prototype this, but maybe there is some other consideration. I'd like to get perspective from yall before working on it. What do you think?
core/emitter.js
Outdated
@@ -30,8 +29,10 @@ class Emitter extends EventEmitter { | |||
} | |||
|
|||
handleDOM(event, ...args) { | |||
const target = (event.composedPath ? event.composedPath()[0] : event.target); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a listener exists above a shadow root, but the event fires from within said shadow root, the target will be the host of the shadow root rather than the element which fired the event.
composedPath
will handle this, though it looks like support for it isn't well documented.
core/selection.js
Outdated
@@ -22,12 +22,13 @@ class Selection { | |||
this.composing = false; | |||
this.mouseDown = false; | |||
this.root = this.scroll.domNode; | |||
this.rootDocument = (this.root.getRootNode ? this.root.getRootNode() : document); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I like this approach as it creates an extra state and scope dependency. A generalized solution would be much preferred which would also cut down on code duplication in any other modules. Can we use ownerDocument
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
afaik ownerDocument
will always be the owning document, not the shadow root (a document fragment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A possible solution for now:
this.rootDocument = this.getDomRoot(this.root)
getDomRoot(ctx) {
if (ctx === document) {
return ctx
}
if (HTMLElement.prototype.attachShadow) {
if (ctx instanceof ShadowRoot) {
if (typeof ctx.getSelection === 'function') {
return ctx
}
return document
}
}
return this.getDomRoot(ctx.parentNode)
}
A modified variation from here.
core/selection.js
Outdated
this.emitter.listenDOM('mousedown', document.body, () => { | ||
const node = (this.rootDocument === document ? document.body : this.rootNode); | ||
|
||
this.emitter.listenDOM('mousedown', node, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to know if you drag outside of the boundaries of the editor container. See the lines below this one.
Let me know if I can help you run tests or figure out a specific issue! Very interested in this as well! |
it just needs tests writing now i think. there's still some comments @jhchen hasn't responded to but the code is fine either way. i won't be massively available during christmas but will try get the tests written soon. |
Which comments require my response? |
just whether we are ok with the |
i've added some tests now. just the basics. to be honest we could test literally anything we already test in non-shadow dom. so ive just tested that the common things work. |
Sorry from your comment I thought you were going to think about it some more and come back and it is tricky to solve both needs. But to answer your followup no it is not okay to use the memory leaky EMITTERS array. A huge portion of users use Quill in a single page app where memory leaks are a big concern. There are some Github Issues on the subject of SPAs and memory leaks if you want more details. |
yup my mistake, should have updated you. i did have a think about it. the old method is now impossible and was a little unfortunate its self too, because of having to arbitrarily attach instances to dom nodes. the current method is correct IMO other than the fact we have no way of knowing when an instance has been disposed of... a memory leak is important to everyone, whether there are a lot of SPAs concerned with it or not isn't relevant. this leak will occur when you repeatedly make new instances over time (which is something people should not be doing, but we know it will happen). the solution to that is to have teardown somewhere, possibly in the constructor we can check if old instances are still in use. |
@@ -21,6 +19,7 @@ class Emitter extends EventEmitter { | |||
constructor() { | |||
super(); | |||
this.listeners = {}; | |||
EMITTERS.push(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jhchen we could possible iterate through EMITTERS
here, looking for any with a container
which is no longer in the DOM tree. this isn't straight forward, though, because document.body.contains(node)
is false for nodes in shadow trees. we would have to go up the tree (getRootNode
) until we reach a node which is in document
, if ever.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah this won't work, we don't have any idea what container
is at this point
@@ -37,6 +37,42 @@ describe('Selection', function() { | |||
}); | |||
}); | |||
|
|||
describe('shadow root', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jhchen am i ok to just disable these where attachShadow
isn't supported? firefox tests will fail until i do as they're unusually far behind in implementing the shadow dom spec.
I tried this fork in a real application in Polymer 2 and I found two issues: 1- no event is handled: as it was said,
will always be false, as node.contains do not work across shadow root boundaries; hence no event will be targeted, hence no toolbar will be displayed (if bubble) and no action on the selection done. writing
of course will work 2- if the event works, I found a bug on Safari where we have this problem with .getSelection(), as the API DocumentOrShadowRoot.getSelection() is not yet implemented ... there is a bug open on Safari, here all the docs: ProseMirror/prosemirror#476 for Safari the only way to make it work, currently, is to force shadydom on loading |
ah much appreciated @giona69 , its helpful to have someone try it in a real world project. your first issue, im surprised i didn't notice the the second one, we can't really start shipping polyfills etc. we should probably either not support shadow roots in safari for now or find some workaround when we detect that |
@43081j I will go on developing on this and test it ... forcing shadydom on safari for getSelection I do not see any workaround unfortunately, but will go on searching for an alterntive. |
So no Firefox support and major issue in Safari (though Webkit is used by Chrome too?). Is Chrome the only browser ready for this? |
@jhchen shadow dom v1 is shipped in chrome, safari and firefox AFAIK. im not sure what the best way for us to work around the safari bug is. if not for that, it would work fine there too. firefox should work as they shipped much of shadow dom v1 recently. we also have a bug with event handlers according to giona's post. ill get on that today if i can |
@giona69 can you give the latest branch a try with your project if you don't mind? i added a small helper for the |
@jhchen |
@jhchen As of now, Shadow DOM v1 is supported by both Chrome and Safari, and will land in Firefox 63 (can be already tested in beta). Any chance for this PR to be revisited? |
b27ccb9
to
24e1e00
Compare
What needs to happen to move this PR along? I'm happy to help |
0b748a7
to
ce39202
Compare
ive rebased onto latest master, ill have another read through the diff and see if i can remember where we got to. |
cool @43081j , thanks for the quick response |
@mrigdon summary:
i guess to get this fixed up, we need a solution for safari. that could be to just not support shadow dom, i.e. default to and to check the latest codebase for any new occurrences of accessing |
sounds good. i'll play around with it @43081j |
Hello, I just wanted to check in on this issue, and point out a new proposal for a Shadow DOM compatible Selection API. This proposal has been being discussed on WICG/webcomponents#79. In particular, this comment gives a quick TL;DR summary of the proposal. In particular, I'd be curious to hear the opinions of the Quill maintainers as to whether this proposal will suit your needs:
I'd love to hear your feedback either here in this issue, or (better) on the Selection API thread. These changes are still very much in development, so your feedback can have an impact. Thanks! |
This is still WIP.
Trying to add support for quill being used inside a shadow root. This involves:
document
wherever necessary (e.g. when getting selections)querySelector
won't penetrate through shadow boundaries)The primary issue here is that anywhere we try find editor nodes or get selections will not work if the editor is in a shadow root as none of these operations penetrate the boundaries of shadow DOM.
TODO & request for help/remarks:
<br>
(viagetBoundingClientRect
) is off by 10 pixels in height