relatedNode should be retargeted respecting slots
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 28 Sep 2015 22:11:08 +0000 (22:11 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 28 Sep 2015 22:11:08 +0000 (22:11 +0000)
https://bugs.webkit.org/show_bug.cgi?id=149591

Reviewed by Antti Koivisto.

Source/WebCore:

This patch retargets relatedNode with respect to shadow boundaries after r190214 as specified in
https://w3c.github.io/webcomponents/spec/shadow/#retargeting-relatedtarget

Naively implementing the spec'ed behavior can result in O(n^2) behavior since we need to find the common tree scope
ancestor for each target in the event path. This patch avoids this by implementing an O(1) incremental update step
for when target's tree scope changes in the event path. See the description for moveToNewTreeScope below.

Test: fast/shadow-dom/event-with-related-target.html

* dom/EventContext.h: Replaced toMouseOrFocusEventContext by downcast<MouseOrFocusEventContext>.
* dom/EventDispatcher.cpp:
(WebCore::EventRelatedNodeResolver): Removed the code for relatedNode. This class is now only used for touch events.
(WebCore::EventPath): Added m_event as a member variable.

(WebCore::RelatedNodeRetargeter): Added.

(WebCore::RelatedNodeRetargeter::RelatedNodeRetargeter): Does the initial retargeting of relatedNode. When the
tree scope of relatedNode and the target are the same, we immediately exit without collecting ancestor tree scopes
of relatedNode as an optimization. We also special case when the relatedNode and the target are in two different
documents (relatedNode should be nullptr) and when one is in document and the other one is not in the document
(relatedNode should be the host of the outermost shadow root). Otherwise we have to do the real work by collecting
all tree scope ancestors and walking downwards from the document tree scope (note target and relatedNode share the
same document scope here since we would have exited early otherwise).

(WebCore::RelatedNodeRetargeter::currentNode): Returned relatedNode retargeted for the current tree scope.

(WebCore::RelatedNodeRetargeter::moveToNewTreeScope): Moves to a new tree scope. If the original target and
relatedNode were in different trees, there is nothing to be done. Note that we can only move out of a shadow root
to its host or move into a slot so newTreeScope (current target's tree scope) and previousTreeScope (previous
target's tree scope) must have a child-parent relationship.

If previousTreeScope did not contain the retargeted relatedNode, then neither can its child shadow trees. Thus,
there is nothing to be done when moving into a slot in this case. If we're moving out of previousTreeScope, then
newTreeScope may contain the retargeted relatedNode but that still doesn't require any work. So we exit early in
both cases.

Otherwise (previousTreeScope contained retargeted relatedNode), if we're moving out of a child shadow root
(previousTreeScope) then relatedNode should also move to previousTreeScope's shadow host since previousTreeScope
is a direct-child shadow tree of newTreeScope and previousTreeScope's shadow host resides in newTreeScope.

If we're moving into a child shadow root via a slot, then there are three possibilities: relatedNode is in
previousTreeScope, newTreeScope and its child shadow trees, or newTreeScope's sibling tree scopes and its children.
If it is in previousTreeScope (m_lowestCommonAncestorIndex is zero) or in newTreeScope's sibling, then
previousTreeScope is the lowest common tree scope ancestor so there is nothing to be done. If relatedNode is in
newTreeScope, then the retargeted relatedNode is either the shadow host of the shadow tree that contains
relatedNode or relatedNode itself.

(WebCore::RelatedNodeRetargeter::checkConsistency): Finds the retargeted relatedNode in the simplest way to verify
the correctness of the algorithm. We can disable this consistency check if it slows down debug builds too much.
(WebCore::RelatedNodeRetargeter::nodeInLowestCommonAncestor): Finds the
(WebCore::RelatedNodeRetargeter::collectTreeScopes):
(WebCore::EventPath::setRelatedTarget): Rewritten using RelatedNodeRetargeter.

LayoutTests:

Added a new testharness.js test for retargeting relatedNode.

* fast/shadow-dom/event-with-related-target.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@190288 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/fast/shadow-dom/event-with-related-target-expected.txt [new file with mode: 0644]
LayoutTests/fast/shadow-dom/event-with-related-target.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/EventContext.h
Source/WebCore/dom/EventDispatcher.cpp

index c3e567fb0d6f404e2e54f32b44e1dce8d42f3422..70872a4a22f942b9b465ca7cddaf686e5e0b2a26 100644 (file)
@@ -1,3 +1,14 @@
+2015-09-28  Ryosuke Niwa  <rniwa@webkit.org>
+
+        relatedNode should be retargeted respecting slots
+        https://bugs.webkit.org/show_bug.cgi?id=149591
+
+        Reviewed by Antti Koivisto.
+
+        Added a new testharness.js test for retargeting relatedNode.
+
+        * fast/shadow-dom/event-with-related-target.html: Added.
+
 2015-09-28  Saam barati  <sbarati@apple.com>
 
         js/regress/getter-richards-try-catch is timing out on debug layout tests
diff --git a/LayoutTests/fast/shadow-dom/event-with-related-target-expected.txt b/LayoutTests/fast/shadow-dom/event-with-related-target-expected.txt
new file mode 100644 (file)
index 0000000..06b03a0
--- /dev/null
@@ -0,0 +1,20 @@
+
+PASS Firing an event at B1a with relatedNode at B1 with open mode shadow trees 
+PASS Firing an event at B1a with relatedNode at B1 with closed mode shadow trees 
+PASS Firing an event at B1a with relatedNode at B1b1 with open mode shadow trees 
+PASS Firing an event at B1a with relatedNode at B1b1 with closed mode shadow trees 
+PASS Firing an event at B1b1 with relatedNode at B1a with open mode shadow trees 
+PASS Firing an event at B1b1 with relatedNode at B1a with closed mode shadow trees 
+PASS Firing an event at B1a with relatedNode at D1 with open mode shadow trees 
+PASS Firing an event at B1a with relatedNode at D1 with closed mode shadow trees 
+PASS Firing an event at D1 with relatedNode at B1a with open mode shadow trees 
+PASS Firing an event at D1 with relatedNode at B1a with closed mode shadow trees 
+PASS Firing an event at B1a with relatedNode at A1a with open mode shadow trees 
+PASS Firing an event at B1a with relatedNode at A1a with closed mode shadow trees 
+PASS Firing an event at B1a with relatedNode at A1a with open mode shadow trees 
+PASS Firing an event at B1a with relatedNode at A1a with closed mode shadow trees 
+PASS Firing an event at B1a with relatedNode at A1a with open mode shadow trees 
+PASS Firing an event at B1a with relatedNode at A1a with closed mode shadow trees 
+PASS Firing an event at B1a with relatedNode at A1a with open mode shadow trees 
+PASS Firing an event at B1a with relatedNode at A1a with closed mode shadow trees 
+
diff --git a/LayoutTests/fast/shadow-dom/event-with-related-target.html b/LayoutTests/fast/shadow-dom/event-with-related-target.html
new file mode 100644 (file)
index 0000000..aec7113
--- /dev/null
@@ -0,0 +1,343 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Shadow DOM: Firing an event with relatedTarget inside a shadow tree</title>
+    <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+    <meta name="assert" content="The retargeting algorithm is used to determine relative targets">
+    <link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#retargeting-relatedtarget">
+    <script src="../../resources/testharness.js"></script>
+    <script src="../../resources/testharnessreport.js"></script>
+    <link rel='stylesheet' href='../../resources/testharness.css'>
+</head>
+<body>
+    <div id="log"></div>
+    <script>
+
+        function dispatchEventWithLog(shadow, target, event) {
+            var eventPath = [];
+            var relatedTargets = [];
+
+            var attachedNodes = [];
+            for (var nodeKey in shadow) {
+                var startingNode = shadow[nodeKey];
+                for (var node = startingNode; node; node = node.parentNode) {
+                    if (attachedNodes.indexOf(node) >= 0)
+                        continue;
+                    attachedNodes.push(node);
+                    node.addEventListener(event.type, (function (event) {
+                        eventPath.push(this.label);
+                        relatedTargets.push(event.relatedTarget.label);
+                    }).bind(node));
+                }
+            }
+
+            target.dispatchEvent(event);
+
+            return {eventPath: eventPath, relatedTargets: relatedTargets};
+        }
+
+        /*
+        -SR: ShadowRoot  -S: Slot
+        A ------------------------------- A-SR
+        + B ------------ B-SR             + A1 --- A1-SR
+          + C            + B1 --- B1-SR   + A2-S   + A1a
+          + D --- D-SR   + B1a    + B1b --- B1b-SR
+                  + D1            + B1c-S   + B1b1
+                                            + B1b2
+        */
+        function createTestTree(mode) {
+            var namedNodes = {};
+
+            function element(name) {
+                var element = document.createElement(name.indexOf('-S') > 0 ? 'slot' : 'div');
+                element.label = name;
+                namedNodes[name] = element;
+                for (var i = 1; i < arguments.length; i++) {
+                    var item = arguments[i];
+                    if (typeof(item) == 'function')
+                        item(element);
+                    else
+                        element.appendChild(item);
+                }
+                return element;
+            }
+
+            function shadow(name) {
+                var children = [];
+                for (var i = 1; i < arguments.length; i++)
+                    children.push(arguments[i]);
+                return function (element) {
+                    var shadowRoot = element.attachShadow({mode: mode});
+                    shadowRoot.label = name;
+                    namedNodes[name] = shadowRoot;
+                    for (var child of children)
+                        shadowRoot.appendChild(child);
+                }
+            }
+
+            var host = element('A',
+                shadow('A-SR',
+                    element('A1',
+                        shadow('A1-SR',
+                            element('A1a'))),
+                    element('A2-S')
+                ),
+                element('B',
+                    shadow('B-SR',
+                        element('B1',
+                            shadow('B1-SR',
+                                element('B1b',
+                                    shadow('B1b-SR',
+                                        element('B1b1'),
+                                        element('B1b2'))),
+                                element('B1c-S')),
+                            element('B1a'))),
+                    element('C'),
+                    element('D',
+                        shadow('D-SR',
+                            element('D1')))));
+
+            return namedNodes;
+        }
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A (8) --------------------------------------------- A-SR (7)
+        + B (5) [5-8] --- B-SR (4)                          + A1 -------- A1-SR
+          + C             + B1 (3) [*; 0-4] --- B1-SR (2)   + A2-S (6)    + A1a
+          + D --- D-SR      + B1a (*; 0)        + B1b [1,2] --- B1b-SR
+                  + D1                          + B1c-S (1)     + B1b1
+                                                                + B1b2
+        */
+        function testEventAtB1aWithB1a(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                log = dispatchEventWithLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.B1}));
+
+                assert_array_equals(log.eventPath,
+                    ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets,
+                    ['B1',  'B1',    'B1',    'B1', 'B1',   'B', 'B',    'B',    'B'], 'The related targets must be correct.');
+
+            }, 'Firing an event at B1a with relatedNode at B1 with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtB1aWithB1a('open');
+        testEventAtB1aWithB1a('closed');
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A (8) -------------------------------------------- A-SR (7)
+        + B (5) [5-8] --- B-SR (4)                         + A1 ------ A1-SR
+          + C             + B1 (3) [0,3-4] --- B1-SR (2)   + A2-S (6)  + A1a
+          + D --- D-SR      + B1a (*; 0)       + B1b [1,2] --- B1b-SR
+                  + D1                         + B1c-S (1)     + B1b1 [*]
+                                                               + B1b2
+        */
+        function testEventAtB1aWithB1b1(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                log = dispatchEventWithLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.B1b1}));
+
+                assert_array_equals(log.eventPath,
+                    ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets,
+                    ['B1',  'B1b',   'B1b',   'B1', 'B1',   'B', 'B',    'B',    'B'], 'The related targets must be correct.');
+
+            }, 'Firing an event at B1a with relatedNode at B1b1 with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtB1aWithB1b1('open');
+        testEventAtB1aWithB1b1('closed');
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A (9) ------------------------------------------------------- A-SR (8)
+        + B (6) [6-9] --- B-SR (5)                                    + A1 ------ A1-SR
+          + C             + B1 (4) --------- B1-SR (3)                + A2-S (7)  + A1a
+          + D --- D-SR      + B1a [*; 0-5]   + B1b (2) --- B1b-SR (1)
+                  + D1                       + B1c-S       + B1b1 (*; 0)
+                                                           + B1b2
+        */
+        function testEventAtB1b1WithB1a(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                log = dispatchEventWithLog(nodes, nodes.B1b1, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.B1a}));
+
+                assert_array_equals(log.eventPath,
+                    ['B1b1', 'B1b-SR', 'B1b', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets,
+                    ['B1a',  'B1a',    'B1a', 'B1a',   'B1a', 'B1a', 'B', 'B',    'B',    'B'], 'The related targets must be correct.');
+
+            }, 'Firing an event at B1b1 with relatedNode at B1a with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtB1b1WithB1a('open');
+        testEventAtB1b1WithB1a('closed');
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A (8) -------------------------------------------------- A-SR (7)
+        + B (5) -------------- B-SR (4)                          + A1 -------- A1-SR
+          + C                  + B1 (3) [*; 0-4] --- B1-SR (2)   + A2-S (6)    + A1a
+          + D [0-8] --- D-SR     + B1a (*; 0)        + B1b ------ B1b-SR
+                        + D1 [*]                     + B1c-S (1)  + B1b1
+                                                                  + B1b2
+        */
+        function testEventAtB1aWithD1(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                log = dispatchEventWithLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.D1}));
+
+                assert_array_equals(log.eventPath,
+                    ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets,
+                    ['D', 'D', 'D', 'D', 'D', 'D', 'D', 'D', 'D'], 'The related targets must be correct.');
+
+            }, 'Firing an event at B1a with relatedNode at D1 with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtB1aWithD1('open');
+        testEventAtB1aWithD1('closed');
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A (6) ----------------------------------- A-SR (5)
+        + B (3) [0] ----------- B-SR              + A1 ------ A1-SR
+          + C                   + B1 ----- B1-SR  + A2-S (4)  + A1a
+          + D (2) --- D-SR (1)  + B1a [*]  + B1b --- B1b-SR
+                        + D1 (*; 0)         + B1c-S   + B1b1
+                                                      + B1b2
+        */
+        function testEventAtD1WithB1a(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                log = dispatchEventWithLog(nodes, nodes.D1, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.B1a}));
+
+                assert_array_equals(log.eventPath,
+                    ['D1', 'D-SR', 'D', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets,
+                    ['B', 'B', 'B', 'B', 'B', 'B', 'B'], 'The related targets must be correct.');
+
+            }, 'Firing an event at D1 with relatedNode at B1a with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtD1WithB1a('open');
+        testEventAtD1WithB1a('closed');
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A (8) [0-5,8] ---------------------------------------- A-SR (7)
+        + B (5)  ------- B-SR (4)                              + A1 [6,7] --- A1-SR
+          + C            + B1 (3) ----- B1-SR (2)              + A2-S (6)     + A1a [*]
+          + D --- D-SR   + B1a (*; 0)   + B1b ------- B1b-SR
+                  + D1                  + B1c-S (1)   + B1b1
+                                                      + B1b2
+        */
+        function testEventAtB1aWithA1a(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                log = dispatchEventWithLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.A1a}));
+
+                assert_array_equals(log.eventPath,
+                    ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets,
+                    ['A',   'A',     'A',     'A',   'A',   'A', 'A1',   'A1',   'A'], 'The related targets must be correct.');
+
+            }, 'Firing an event at B1a with relatedNode at A1a with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtB1aWithA1a('open');
+        testEventAtB1aWithA1a('closed');
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A (4) ----------------------------------------- A-SR (3)
+        + B [0-4]  ----- B-SR                           + A1 (2) --- A1-SR (1)
+          + C            + B1 ------- B1-SR             + A2-S       + A1a (*; 0)
+          + D --- D-SR     + B1a [*]  + B1b --- B1b-SR
+                  + D1                + B1c-S   + B1b1
+                                                + B1b2
+        */
+        function testEventAtA1aWithB1a(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                log = dispatchEventWithLog(nodes, nodes.A1a, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.B1a}));
+
+                assert_array_equals(log.eventPath,
+                    ['A1a', 'A1-SR', 'A1', 'A-SR', 'A'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets,
+                    ['B', 'B', 'B', 'B', 'B'], 'The related targets must be correct.');
+
+            }, 'Firing an event at B1a with relatedNode at A1a with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtA1aWithB1a('open');
+        testEventAtA1aWithB1a('closed');
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A (8) ----------------------------------- A-SR (7)
+        + B (5)  ----- B-SR (4)                   + A2-S (6)
+          + C          + B1 (3) ----- B1-SR (2)
+          + D --- D-SR   + B1a (*; 0) + B1b ------- B1b-SR
+                  + D1                + B1c-S (1)   + B1b1
+                                                    + B1b2
+        A1 [0-6] --- A1-SR
+                   + A1a [*]
+        */
+        function testEventAtB1aWithDetachedA1a(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                nodes['A-SR'].removeChild(nodes.A1);
+                log = dispatchEventWithLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.A1a}));
+
+                assert_array_equals(log.eventPath,
+                    ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets,
+                    ['A1', 'A1', 'A1', 'A1', 'A1', 'A1', 'A1', 'A1', 'A1'], 'The related targets must be correct.');
+
+            }, 'Firing an event at B1a with relatedNode at A1a with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtB1aWithDetachedA1a('open');
+        testEventAtB1aWithDetachedA1a('closed');
+
+        /*
+        -SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+        A ----------------------------------- A-SR
+        + B [0-3]  ----- B-SR                 + A2-S
+          + C            + B1 -------- B1-SR
+          + D --- D-SR     + B1a [*]   + B1b --- B1b-SR
+                  + D1                 + B1c-S   + B1b1
+                                                 + B1b2
+        A1 (2) --- A1-SR (1)
+                   + A1a (*; 0)
+        */
+        function testEventAtA1aWithDetachedB1a(mode) {
+            test(function () {
+                var nodes = createTestTree(mode);
+
+                nodes['A-SR'].removeChild(nodes.A1);
+                log = dispatchEventWithLog(nodes, nodes.A1a, new MouseEvent('foo', {bubbles: true, relatedTarget: nodes.B1a}));
+
+                assert_array_equals(log.eventPath,      ['A1a', 'A1-SR', 'A1'], 'The event path must be correct.');
+                assert_array_equals(log.relatedTargets, ['B',   'B',     'B' ], 'The related targets must be correct.');
+
+            }, 'Firing an event at B1a with relatedNode at A1a with ' + mode + ' mode shadow trees');
+        }
+
+        testEventAtA1aWithDetachedB1a('open');
+        testEventAtA1aWithDetachedB1a('closed');
+
+    </script>
+    </body>
+</html>
index f341f7f6a7aeb71729ce04c283630c89ed479415..58fb6f1335157128a09263e91d4e7a7919b9fcb3 100644 (file)
@@ -1,3 +1,63 @@
+2015-09-28  Ryosuke Niwa  <rniwa@webkit.org>
+
+        relatedNode should be retargeted respecting slots
+        https://bugs.webkit.org/show_bug.cgi?id=149591
+
+        Reviewed by Antti Koivisto.
+
+        This patch retargets relatedNode with respect to shadow boundaries after r190214 as specified in
+        https://w3c.github.io/webcomponents/spec/shadow/#retargeting-relatedtarget
+
+        Naively implementing the spec'ed behavior can result in O(n^2) behavior since we need to find the common tree scope
+        ancestor for each target in the event path. This patch avoids this by implementing an O(1) incremental update step
+        for when target's tree scope changes in the event path. See the description for moveToNewTreeScope below.
+
+        Test: fast/shadow-dom/event-with-related-target.html
+
+        * dom/EventContext.h: Replaced toMouseOrFocusEventContext by downcast<MouseOrFocusEventContext>.
+        * dom/EventDispatcher.cpp:
+        (WebCore::EventRelatedNodeResolver): Removed the code for relatedNode. This class is now only used for touch events.
+        (WebCore::EventPath): Added m_event as a member variable.
+
+        (WebCore::RelatedNodeRetargeter): Added.
+
+        (WebCore::RelatedNodeRetargeter::RelatedNodeRetargeter): Does the initial retargeting of relatedNode. When the
+        tree scope of relatedNode and the target are the same, we immediately exit without collecting ancestor tree scopes
+        of relatedNode as an optimization. We also special case when the relatedNode and the target are in two different
+        documents (relatedNode should be nullptr) and when one is in document and the other one is not in the document
+        (relatedNode should be the host of the outermost shadow root). Otherwise we have to do the real work by collecting
+        all tree scope ancestors and walking downwards from the document tree scope (note target and relatedNode share the
+        same document scope here since we would have exited early otherwise).
+
+        (WebCore::RelatedNodeRetargeter::currentNode): Returned relatedNode retargeted for the current tree scope.
+
+        (WebCore::RelatedNodeRetargeter::moveToNewTreeScope): Moves to a new tree scope. If the original target and
+        relatedNode were in different trees, there is nothing to be done. Note that we can only move out of a shadow root
+        to its host or move into a slot so newTreeScope (current target's tree scope) and previousTreeScope (previous
+        target's tree scope) must have a child-parent relationship.
+
+        If previousTreeScope did not contain the retargeted relatedNode, then neither can its child shadow trees. Thus,
+        there is nothing to be done when moving into a slot in this case. If we're moving out of previousTreeScope, then
+        newTreeScope may contain the retargeted relatedNode but that still doesn't require any work. So we exit early in
+        both cases.
+
+        Otherwise (previousTreeScope contained retargeted relatedNode), if we're moving out of a child shadow root
+        (previousTreeScope) then relatedNode should also move to previousTreeScope's shadow host since previousTreeScope
+        is a direct-child shadow tree of newTreeScope and previousTreeScope's shadow host resides in newTreeScope.
+
+        If we're moving into a child shadow root via a slot, then there are three possibilities: relatedNode is in
+        previousTreeScope, newTreeScope and its child shadow trees, or newTreeScope's sibling tree scopes and its children.
+        If it is in previousTreeScope (m_lowestCommonAncestorIndex is zero) or in newTreeScope's sibling, then
+        previousTreeScope is the lowest common tree scope ancestor so there is nothing to be done. If relatedNode is in
+        newTreeScope, then the retargeted relatedNode is either the shadow host of the shadow tree that contains
+        relatedNode or relatedNode itself.
+
+        (WebCore::RelatedNodeRetargeter::checkConsistency): Finds the retargeted relatedNode in the simplest way to verify
+        the correctness of the algorithm. We can disable this consistency check if it slows down debug builds too much.
+        (WebCore::RelatedNodeRetargeter::nodeInLowestCommonAncestor): Finds the 
+        (WebCore::RelatedNodeRetargeter::collectTreeScopes):
+        (WebCore::EventPath::setRelatedTarget): Rewritten using RelatedNodeRetargeter.
+
 2015-09-28  Alex Christensen  <achristensen@webkit.org>
 
         Build WK1 with CMake on Mac
index aa0cfb267ea17c591857b3edee82f1922c47dad8..3f62460d7f5853fe6cf67455313cf9cbc232a727 100644 (file)
@@ -76,12 +76,6 @@ private:
     RefPtr<EventTarget> m_relatedTarget;
 };
 
-inline MouseOrFocusEventContext& toMouseOrFocusEventContext(EventContext& eventContext)
-{
-    ASSERT_WITH_SECURITY_IMPLICATION(eventContext.isMouseOrFocusEventContext());
-    return static_cast<MouseOrFocusEventContext&>(eventContext);
-}
-
 
 #if ENABLE(TOUCH_EVENTS) && !PLATFORM(IOS)
 class TouchEventContext final : public EventContext {
@@ -162,4 +156,8 @@ inline void MouseOrFocusEventContext::setRelatedTarget(PassRefPtr<EventTarget> r
 
 }
 
+SPECIALIZE_TYPE_TRAITS_BEGIN(WebCore::MouseOrFocusEventContext)
+static bool isType(const WebCore::EventContext& context) { return context.isMouseOrFocusEventContext(); }
+SPECIALIZE_TYPE_TRAITS_END()
+
 #endif // EventContext_h
index 3c529c2e8c12e15582246125e8bb8eef33a0a3f5..0cd459964f688ad59bf19f5d4c51c72e462de184 100644 (file)
@@ -2,7 +2,7 @@
  * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
  *           (C) 1999 Antti Koivisto (koivisto@kde.org)
  *           (C) 2001 Dirk Mueller (mueller@kde.org)
- * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2015 Apple Inc. All rights reserved.
  * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
  * Copyright (C) 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
  * Copyright (C) 2010, 2011, 2012, 2013 Google Inc. All rights reserved.
@@ -101,24 +101,14 @@ private:
     void updateTouchListsInEventPath(const TouchList*, TouchEventContext::TouchListType);
 #endif
 
+    Event& m_event;
     Vector<std::unique_ptr<EventContext>, 32> m_path;
 };
 
+#if ENABLE(TOUCH_EVENTS) && !PLATFORM(IOS)
+// FIXME: Use RelatedNodeRetargeter instead.
 class EventRelatedNodeResolver {
 public:
-    EventRelatedNodeResolver(Node& relatedNode)
-        : m_relatedNode(relatedNode)
-        , m_relatedNodeTreeScope(relatedNode.treeScope())
-        , m_relatedNodeInCurrentTreeScope(nullptr)
-        , m_currentTreeScope(nullptr)
-#if ENABLE(TOUCH_EVENTS) && !PLATFORM(IOS)
-        , m_touch(0)
-        , m_touchListType(TouchEventContext::NotTouchList)
-#endif
-    {
-    }
-
-#if ENABLE(TOUCH_EVENTS) && !PLATFORM(IOS)
     EventRelatedNodeResolver(Touch& touch, TouchEventContext::TouchListType touchListType)
         : m_relatedNode(*touch.target()->toNode())
         , m_relatedNodeTreeScope(m_relatedNode.treeScope())
@@ -129,12 +119,9 @@ public:
     {
         ASSERT(touch.target()->toNode());
     }
-#endif
 
-#if ENABLE(TOUCH_EVENTS) && !PLATFORM(IOS)
     Touch* touch() const { return m_touch; }
     TouchEventContext::TouchListType touchListType() const { return m_touchListType; }
-#endif
 
     Node* moveToParentOrShadowHost(Node& newTarget)
     {
@@ -197,11 +184,10 @@ private:
     const TreeScope& m_relatedNodeTreeScope;
     Node* m_relatedNodeInCurrentTreeScope;
     TreeScope* m_currentTreeScope;
-#if ENABLE(TOUCH_EVENTS) && !PLATFORM(IOS)
     Touch* m_touch;
     TouchEventContext::TouchListType m_touchListType;
-#endif
 };
+#endif
 
 inline EventTarget* eventTargetRespectingTargetRules(Node& referenceNode)
 {
@@ -415,6 +401,7 @@ static Node* nodeOrHostIfPseudoElement(Node* node)
 }
 
 EventPath::EventPath(Node& originalTarget, Event& event)
+    : m_event(event)
 {
 #if ENABLE(SHADOW_DOM)
     Vector<EventTarget*, 16> targetStack;
@@ -513,30 +500,170 @@ bool EventPath::updateTouchLists(const TouchEvent& touchEvent)
 }
 #endif
 
+class RelatedNodeRetargeter {
+public:
+    RelatedNodeRetargeter(Node& relatedNode, TreeScope& targetTreeScope)
+        : m_relatedNode(relatedNode)
+        , m_retargetedRelatedNode(&relatedNode)
+    {
+        TreeScope* currentTreeScope = &m_relatedNode.treeScope();
+        if (LIKELY(currentTreeScope == &targetTreeScope))
+            return;
+
+        if (&currentTreeScope->documentScope() != &targetTreeScope.documentScope()) {
+            m_hasDifferentTreeRoot = true;
+            m_retargetedRelatedNode = nullptr;
+            return;
+        }
+        if (relatedNode.inDocument() != targetTreeScope.rootNode().inDocument()) {
+            m_hasDifferentTreeRoot = true;
+            while (m_retargetedRelatedNode->isInShadowTree())
+                m_retargetedRelatedNode = downcast<ShadowRoot>(m_retargetedRelatedNode->treeScope().rootNode()).host();
+            return;
+        }
+
+        collectTreeScopes();
+
+        // FIXME: We should collect this while constructing the event path.
+        Vector<TreeScope*, 8> targetTreeScopeAncestors;
+        for (TreeScope* currentTreeScope = &targetTreeScope; currentTreeScope; currentTreeScope = currentTreeScope->parentTreeScope())
+            targetTreeScopeAncestors.append(currentTreeScope);
+        ASSERT_WITH_SECURITY_IMPLICATION(!targetTreeScopeAncestors.isEmpty());
+
+        unsigned i = m_ancestorTreeScopes.size();
+        unsigned j = targetTreeScopeAncestors.size();
+        ASSERT_WITH_SECURITY_IMPLICATION(m_ancestorTreeScopes.last() == targetTreeScopeAncestors.last());
+        while (m_ancestorTreeScopes[i - 1] == targetTreeScopeAncestors[j - 1]) {
+            i--;
+            j--;
+            if (!i || !j)
+                break;
+        }
+
+        m_lowestCommonAncestorIndex = i;
+        m_retargetedRelatedNode = nodeInLowestCommonAncestor();
+    }
+
+    Node* currentNode(TreeScope& currentTreeScope)
+    {
+        checkConsistency(currentTreeScope);
+        return m_retargetedRelatedNode;
+    }
+
+    void moveToNewTreeScope(TreeScope* previousTreeScope, TreeScope& newTreeScope)
+    {
+        if (m_hasDifferentTreeRoot)
+            return;
+
+        auto& currentRelatedNodeScope = m_retargetedRelatedNode->treeScope();
+        if (previousTreeScope != &currentRelatedNodeScope) {
+            // currentRelatedNode is still outside our shadow tree. New tree scope may contain currentRelatedNode
+            // but there is no need to re-target it. Moving into a slot (thereby a deeper shadow tree) doesn't matter.
+            return;
+        }
+
+        bool enteredSlot = newTreeScope.parentTreeScope() == previousTreeScope;
+        if (enteredSlot) {
+            if (m_lowestCommonAncestorIndex) {
+                if (m_ancestorTreeScopes.isEmpty())
+                    collectTreeScopes();
+                bool relatedNodeIsInSlot = m_ancestorTreeScopes[m_lowestCommonAncestorIndex - 1] == &newTreeScope;
+                if (relatedNodeIsInSlot) {
+                    m_lowestCommonAncestorIndex--;
+                    m_retargetedRelatedNode = nodeInLowestCommonAncestor();
+                    ASSERT(&newTreeScope == &m_retargetedRelatedNode->treeScope());
+                }
+            } else
+                ASSERT(m_retargetedRelatedNode == &m_relatedNode);
+        } else {
+            ASSERT(previousTreeScope->parentTreeScope() == &newTreeScope);
+            m_lowestCommonAncestorIndex++;
+            ASSERT_WITH_SECURITY_IMPLICATION(m_ancestorTreeScopes.isEmpty() || m_lowestCommonAncestorIndex < m_ancestorTreeScopes.size());
+            m_retargetedRelatedNode = downcast<ShadowRoot>(currentRelatedNodeScope.rootNode()).host();
+            ASSERT(&newTreeScope == &m_retargetedRelatedNode->treeScope());
+        }
+    }
+
+    void checkConsistency(TreeScope& currentTreeScope)
+    {
+#if !ASSERT_DISABLED
+        for (auto* relatedNodeScope = &m_relatedNode.treeScope(); relatedNodeScope; relatedNodeScope = relatedNodeScope->parentTreeScope()) {
+            for (auto* targetScope = &currentTreeScope; targetScope; targetScope = targetScope->parentTreeScope()) {
+                if (targetScope == relatedNodeScope) {
+                    ASSERT(&m_retargetedRelatedNode->treeScope() == relatedNodeScope);
+                    return;
+                }
+            }
+        }
+        ASSERT(!m_retargetedRelatedNode);
+#else
+        UNUSED_PARAM(currentTreeScope);
+#endif
+    }
+
+private:
+    Node* nodeInLowestCommonAncestor()
+    {
+        if (!m_lowestCommonAncestorIndex)
+            return &m_relatedNode;
+        auto& rootNode = m_ancestorTreeScopes[m_lowestCommonAncestorIndex - 1]->rootNode();
+        return downcast<ShadowRoot>(rootNode).host();
+    }
+
+    void collectTreeScopes()
+    {
+        ASSERT(m_ancestorTreeScopes.isEmpty());
+        for (TreeScope* currentTreeScope = &m_relatedNode.treeScope(); currentTreeScope; currentTreeScope = currentTreeScope->parentTreeScope())
+            m_ancestorTreeScopes.append(currentTreeScope);
+        ASSERT_WITH_SECURITY_IMPLICATION(!m_ancestorTreeScopes.isEmpty());
+    }
+
+    Node& m_relatedNode;
+    Node* m_retargetedRelatedNode;
+    Vector<TreeScope*, 8> m_ancestorTreeScopes;
+    unsigned m_lowestCommonAncestorIndex { 0 };
+    bool m_hasDifferentTreeRoot { false };
+};
+
 void EventPath::setRelatedTarget(Node& origin, EventTarget& relatedTarget)
 {
+    UNUSED_PARAM(origin);
     Node* relatedNode = relatedTarget.toNode();
-    if (!relatedNode)
+    if (!relatedNode || m_path.isEmpty())
         return;
 
-    EventRelatedNodeResolver resolver(*relatedNode);
+    RelatedNodeRetargeter retargeter(*relatedNode, downcast<MouseOrFocusEventContext>(*m_path[0]).node()->treeScope());
 
     bool originIsRelatedTarget = &origin == relatedNode;
+    // FIXME: We should add a new flag on Event instead.
+    bool shouldTrimEventPath = m_event.type() == eventNames().mouseoverEvent
+        || m_event.type() == eventNames().mousemoveEvent
+        || m_event.type() == eventNames().mouseoutEvent;
     Node& rootNodeInOriginTreeScope = origin.treeScope().rootNode();
-
-    size_t eventPathSize = m_path.size();
-    size_t i = 0;
-    while (i < eventPathSize) {
-        Node* contextNode = m_path[i]->node();
-        Node* currentRelatedNode = resolver.moveToParentOrShadowHost(*contextNode);
-        if (!originIsRelatedTarget && m_path[i]->target() == currentRelatedNode)
+    TreeScope* previousTreeScope = nullptr;
+    size_t originalEventPathSize = m_path.size();
+    for (unsigned contextIndex = 0; contextIndex < originalEventPathSize; contextIndex++) {
+        auto& context = downcast<MouseOrFocusEventContext>(*m_path[contextIndex]);
+
+        TreeScope& currentTreeScope = context.node()->treeScope();
+        if (UNLIKELY(previousTreeScope && &currentTreeScope != previousTreeScope))
+            retargeter.moveToNewTreeScope(previousTreeScope, currentTreeScope);
+
+        Node* currentRelatedNode = retargeter.currentNode(currentTreeScope);
+        if (UNLIKELY(shouldTrimEventPath && !originIsRelatedTarget && context.target() == currentRelatedNode)) {
+            m_path.shrink(contextIndex);
             break;
-        toMouseOrFocusEventContext(*m_path[i]).setRelatedTarget(currentRelatedNode);
-        i++;
-        if (originIsRelatedTarget && &rootNodeInOriginTreeScope == contextNode)
+        }
+
+        context.setRelatedTarget(currentRelatedNode);
+
+        if (UNLIKELY(shouldTrimEventPath && originIsRelatedTarget && context.node() == &rootNodeInOriginTreeScope)) {
+            m_path.shrink(contextIndex + 1);
             break;
+        }
+
+        previousTreeScope = &currentTreeScope;
     }
-    m_path.shrink(i);
 }
 
 bool EventPath::hasEventListeners(const AtomicString& eventType) const