Add slotchange event
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 14 Mar 2016 12:02:21 +0000 (12:02 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 14 Mar 2016 12:02:21 +0000 (12:02 +0000)
https://bugs.webkit.org/show_bug.cgi?id=155424
<rdar://problem/24997534>

Reviewed by Antti Koivisto.

Source/WebCore:

Added `slotchange` event as discussed on https://github.com/w3c/webcomponents/issues/288.

While the exact semantics of it could still evolve over time, this patch implements as
an asynchronous event that fires on a slot element whenever its distributed nodes change
(flattened assigned nodes):
http://w3c.github.io/webcomponents/spec/shadow/#dfn-distributed-nodes

Since inserting or removing an element from a shadow host could needs to enqueue this event
on the right slot element, this patch moves the invalidation point of element removals and
insertions from Element::childrenChanged to Element::insertedInto and Element::removedFrom.
Text nodes are still invalidated at Element::childrenChanged for performance reasons
since it could only appear within a default slot element.

Because this more fine-grained invalidation needs to be overridden by HTMLDetailsElement,
we now subclass SlotAssignment in HTMLDetailsElement instead of passing in a std::function.

Test: fast/shadow-dom/slotchange-event.html

* dom/Document.cpp:
(WebCore::Document::enqueueSlotchangeEvent): Added.
* dom/Document.h:
* dom/Element.cpp:
(WebCore::Element::attributeChanged): Call hostChildElementDidChangeSlotAttr.
(WebCore::Element::insertedInto): Call hostChildElementDidChange.
(WebCore::Element::removedFrom): Ditto.
(WebCore::Element::childrenChanged): Don't invalidate the slots on ElementInserted and
ElementRemoved since they're now done in Element::insertedInto and Element::removedFrom.
* dom/Event.cpp:
(WebCore::Event::scoped): slotchange event is scoped.
* dom/EventNames.h: Added eventNames().slotchange.
* dom/ShadowRoot.cpp:
(WebCore::ShadowRoot::invalidateSlotAssignments): Deleted.
(WebCore::ShadowRoot::invalidateDefaultSlotAssignments): Deleted.
* dom/ShadowRoot.h:
(ShadowRoot): Added more fine-grained invalidators, mirroring changes to SlotAssignment.
* dom/SlotAssignment.cpp:
(WebCore::SlotAssignment::SlotAssignment): Removed a variant that takes SlotNameFunction
since HTMLDetailsElement now subclasses SlotAssignment.
(WebCore::SlotAssignment::~SlotAssignment): Added now that the class is virtual.
(WebCore::recursivelyFireSlotChangeEvent): Added.
(WebCore::SlotAssignment::didChangeSlot): Added. Invalidates the style tree only if there
is a corresponding slot element, and fires slotchange event. When the slot element we found
in this shadow tree is assigned to a slot element inside an inner shadow tree, recursively
fire slotchange event on each such inner slots.
(WebCore::SlotAssignment::hostChildElementDidChange): Added. Update the matching slot when
an element is inserted or removed under a shadow host.
(WebCore::SlotAssignment::assignedNodesForSlot): Removed the superfluous early exit to an
release assert since addSlotElementByName should always create a SlotInfo for each element.
(WebCore::SlotAssignment::slotNameForHostChild): Added. This is the equivalent of old
m_slotNameFunction which DetailsSlotAssignment overrides.
(WebCore::SlotAssignment::invalidateDefaultSlot): Deleted.
(WebCore::SlotAssignment::findFirstSlotElement): Added an assertion. slotInfo.element must
be nullptr if elementCount is 0, and elementCount must be 0 if slotInfo.element is nullptr
after calling resolveAllSlotElements, which traverses the entire shadow tree to find all
slot elements.
(WebCore::SlotAssignment::assignSlots):
* dom/SlotAssignment.h: Implemented inline functions of ShadowRoot here to avoid including
SlotAssignment.h in ShadowRoot.h. Not inlining them results in extra function calls for all
builtin elements with shadow root without slot elements, which impacts performance.
(WebCore::ShadowRoot::didRemoveAllChildrenOfShadowHost): Added.
(WebCore::ShadowRoot::didChangeDefaultSlot): Added.
(WebCore::ShadowRoot::hostChildElementDidChange): Added.
(WebCore::ShadowRoot::hostChildElementDidChangeSlotAttribute): Added.
(WebCore::ShadowRoot::innerSlotDidChange):
* html/HTMLDetailsElement.cpp:
(WebCore::DetailsSlotAssignment): Added. Subclasses SlotAssignment to override
hostChildElementDidChange and slotNameForHostChild.
(WebCore::DetailsSlotAssignment::hostChildElementDidChange): Added. We don't check if this
is the first summary element since we don't know the answer when this function is called
inside Element::removedFrom.
(WebCore::DetailsSlotAssignment::slotNameForHostChild): Renamed from slotNameFunction. Also
removed the code to return nullAtom when details element is not open as that messes up new
fine-grained invalidation. Insert/remove the slot element in parseAttribute instead.
(WebCore::HTMLDetailsElement::didAddUserAgentShadowRoot): Don't insert the slot element for
the summary since the details element is not open now.
(WebCore::HTMLDetailsElement::parseAttribute): Remove and insert the slot element for the
summary here instead of changing the behavior of slotNameForHostChild.
* html/HTMLDetailsElement.h:
* html/HTMLSlotElement.cpp:
(WebCore::HTMLSlotElement::enqueueSlotChangeEvent): Added. Enqueues a new slotchange event
if we haven't done so for this element yet.
(WebCore::HTMLSlotElement::dispatchEvent): Added. Clear m_hasEnqueuedSlotChangeEvent when
dispatching a slotchange event so that a subsequent call to enqueueSlotChangeEvent would
enqueue a new event. Note scripts call EventTarget::dispatchEventForBindings instead.
* html/HTMLSlotElement.h:

LayoutTests:

Added a W3C style testharness.js test.

* fast/shadow-dom/ShadowRoot-interface-expected.txt:
* fast/shadow-dom/ShadowRoot-interface.html: Don't import testharness.css from svn.webkit.org.
* fast/shadow-dom/slotchange-event-expected.txt: Added.
* fast/shadow-dom/slotchange-event.html: Added.

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

19 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/shadow-dom/ShadowRoot-interface-expected.txt
LayoutTests/fast/shadow-dom/ShadowRoot-interface.html
LayoutTests/fast/shadow-dom/slotchange-event-expected.txt [new file with mode: 0644]
LayoutTests/fast/shadow-dom/slotchange-event.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/Document.cpp
Source/WebCore/dom/Document.h
Source/WebCore/dom/Element.cpp
Source/WebCore/dom/Event.cpp
Source/WebCore/dom/EventNames.h
Source/WebCore/dom/ShadowRoot.cpp
Source/WebCore/dom/ShadowRoot.h
Source/WebCore/dom/SlotAssignment.cpp
Source/WebCore/dom/SlotAssignment.h
Source/WebCore/html/HTMLDetailsElement.cpp
Source/WebCore/html/HTMLDetailsElement.h
Source/WebCore/html/HTMLSlotElement.cpp
Source/WebCore/html/HTMLSlotElement.h

index 1cd8ea6..b1052aa 100644 (file)
@@ -1,3 +1,18 @@
+2016-03-14  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Add slotchange event
+        https://bugs.webkit.org/show_bug.cgi?id=155424
+        <rdar://problem/24997534>
+
+        Reviewed by Antti Koivisto.
+
+        Added a W3C style testharness.js test.
+
+        * fast/shadow-dom/ShadowRoot-interface-expected.txt:
+        * fast/shadow-dom/ShadowRoot-interface.html: Don't import testharness.css from svn.webkit.org.
+        * fast/shadow-dom/slotchange-event-expected.txt: Added.
+        * fast/shadow-dom/slotchange-event.html: Added.
+
 2016-03-13  Darin Adler  <darin@apple.com>
 
         Add copy/paste plug-in check for XHTML document
index 7ffc594..5a53ec7 100644 (file)
@@ -1,4 +1,3 @@
-Blocked access to external URL https://svn.webkit.org/repository/webkit/trunk/LayoutTests/resources/testharness.css
 
 PASS Check the existence of ShadowRoot interface 
 PASS ShadowRoot must inherit from DocumentFragment 
index 52da849..d88e921 100644 (file)
@@ -7,7 +7,7 @@
 <link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#the-shadowroot-interface">
 <script src="../../resources/testharness.js"></script>
 <script src="../../resources/testharnessreport.js"></script>
-<link rel='stylesheet' href='https://svn.webkit.org/repository/webkit/trunk/LayoutTests/resources/testharness.css'>
+<link rel='stylesheet' href='../../resources/testharness.css'>
 </head>
 <body>
 <div id="log"></div>
diff --git a/LayoutTests/fast/shadow-dom/slotchange-event-expected.txt b/LayoutTests/fast/shadow-dom/slotchange-event-expected.txt
new file mode 100644 (file)
index 0000000..fd7028f
--- /dev/null
@@ -0,0 +1,32 @@
+hello
+hello
+
+PASS slotchange event must fire on a default slot element inside an open shadow root  in a document 
+PASS slotchange event must fire on a default slot element inside a closed shadow root  in a document 
+PASS slotchange event must fire on a default slot element inside an open shadow root  not in a document 
+PASS slotchange event must fire on a default slot element inside a closed shadow root  not in a document 
+PASS slotchange event must fire on a named slot element insidean open shadow root  in a document 
+PASS slotchange event must fire on a named slot element insidea closed shadow root  in a document 
+PASS slotchange event must fire on a named slot element insidean open shadow root  not in a document 
+PASS slotchange event must fire on a named slot element insidea closed shadow root  not in a document 
+PASS slotchange event must not fire on a slot element inside an open shadow root  in a document when another slot's assigned nodes change 
+PASS slotchange event must not fire on a slot element inside a closed shadow root  in a document when another slot's assigned nodes change 
+PASS slotchange event must not fire on a slot element inside an open shadow root  not in a document when another slot's assigned nodes change 
+PASS slotchange event must not fire on a slot element inside a closed shadow root  not in a document when another slot's assigned nodes change 
+PASS slotchange event must not fire on a slot element inside an open shadow root  in a document when the shadow host was mutated before the slot was inserted or after the slot was removed 
+PASS slotchange event must not fire on a slot element inside a closed shadow root  in a document when the shadow host was mutated before the slot was inserted or after the slot was removed 
+PASS slotchange event must not fire on a slot element inside an open shadow root  not in a document when the shadow host was mutated before the slot was inserted or after the slot was removed 
+PASS slotchange event must not fire on a slot element inside a closed shadow root  not in a document when the shadow host was mutated before the slot was inserted or after the slot was removed 
+PASS slotchange event must fire on a slot element inside an open shadow root  in a document even if the slot was removed immediately after the assigned nodes were mutated 
+PASS slotchange event must fire on a slot element inside a closed shadow root  in a document even if the slot was removed immediately after the assigned nodes were mutated 
+PASS slotchange event must fire on a slot element inside an open shadow root  not in a document even if the slot was removed immediately after the assigned nodes were mutated 
+PASS slotchange event must fire on a slot element inside a closed shadow root  not in a document even if the slot was removed immediately after the assigned nodes were mutated 
+PASS slotchange event must fire on a slot element inside an open shadow root  in a document when innerHTML modifies the children of the shadow host 
+PASS slotchange event must fire on a slot element inside a closed shadow root  in a document when innerHTML modifies the children of the shadow host 
+PASS slotchange event must fire on a slot element inside an open shadow root  not in a document when innerHTML modifies the children of the shadow host 
+PASS slotchange event must fire on a slot element inside a closed shadow root  not in a document when innerHTML modifies the children of the shadow host 
+PASS slotchange event must fire on a slot element inside an open shadow root  in a document when nested slots's contents change 
+PASS slotchange event must fire on a slot element inside a closed shadow root  in a document when nested slots's contents change 
+PASS slotchange event must fire on a slot element inside an open shadow root  not in a document when nested slots's contents change 
+PASS slotchange event must fire on a slot element inside a closed shadow root  not in a document when nested slots's contents change 
+
diff --git a/LayoutTests/fast/shadow-dom/slotchange-event.html b/LayoutTests/fast/shadow-dom/slotchange-event.html
new file mode 100644 (file)
index 0000000..d4b8bef
--- /dev/null
@@ -0,0 +1,539 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Shadow DOM: slotchange event</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<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 treeName(mode, connectedToDocument)
+{
+    return (mode == 'open' ? 'an ' : 'a ') + mode + ' shadow root '
+        + (connectedToDocument ? '' : ' not') + ' in a document';
+}
+
+function testAppendingSpanToShadowRootWithDefaultSlot(mode, connectedToDocument)
+{
+    var test = async_test('slotchange event must fire on a default slot element inside '
+        + treeName(mode, connectedToDocument));
+
+    var host;
+    var slot;
+    var eventCount = 0;
+
+    test.step(function () {
+        host = document.createElement('div');
+        if (connectedToDocument)
+            document.body.appendChild(host);
+
+        var shadowRoot = host.attachShadow({'mode': mode});
+        slot = document.createElement('slot');
+
+        slot.addEventListener('slotchange', function (event) {
+            if (event.isFakeEvent)
+                return;
+
+            test.step(function () {
+                assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
+                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
+                assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
+            });
+            eventCount++;
+        });
+
+        shadowRoot.appendChild(slot);
+
+        host.appendChild(document.createElement('span'));
+        host.appendChild(document.createElement('b'));
+
+        assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
+    });
+
+    setTimeout(function () {
+        test.step(function () {
+            assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed');
+
+            host.appendChild(document.createElement('i'));
+        });
+
+        setTimeout(function () {
+            test.step(function () {
+                assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed');
+
+                host.appendChild(document.createTextNode('hello'));
+
+                var fakeEvent = new Event('slotchange');
+                fakeEvent.isFakeEvent = true;
+                slot.dispatchEvent(fakeEvent);
+            });
+
+            setTimeout(function () {
+                test.step(function () {
+                    assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed'
+                        + ' event if there was a synthetic slotchange event fired');
+                });
+                test.done();
+            }, 1);
+        }, 1);
+    }, 1);
+}
+
+testAppendingSpanToShadowRootWithDefaultSlot('open', true);
+testAppendingSpanToShadowRootWithDefaultSlot('closed', true);
+testAppendingSpanToShadowRootWithDefaultSlot('open', false);
+testAppendingSpanToShadowRootWithDefaultSlot('closed', false);
+
+function testAppendingSpanToShadowRootWithNamedSlot(mode, connectedToDocument)
+{
+    var test = async_test('slotchange event must fire on a named slot element inside'
+        + treeName(mode, connectedToDocument));
+
+    var host;
+    var slot;
+    var eventCount = 0;
+
+    test.step(function () {
+        host = document.createElement('div');
+        if (connectedToDocument)
+            document.body.appendChild(host);
+
+        var shadowRoot = host.attachShadow({'mode': mode});
+        slot = document.createElement('slot');
+        slot.name = 'someSlot';
+
+        slot.addEventListener('slotchange', function (event) {
+            if (event.isFakeEvent)
+                return;
+
+            test.step(function () {
+                assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
+                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
+                assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
+            });
+            eventCount++;
+        });
+
+        shadowRoot.appendChild(slot);
+
+        var span = document.createElement('span');
+        span.slot = 'someSlot';
+        host.appendChild(span);
+
+        var b = document.createElement('b');
+        b.slot = 'someSlot';
+        host.appendChild(b);
+
+        assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
+    });
+
+    setTimeout(function () {
+        test.step(function () {
+            assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed');
+
+            var i = document.createElement('i');
+            i.slot = 'someSlot';
+            host.appendChild(i);
+        });
+
+        setTimeout(function () {
+            test.step(function () {
+                assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed');
+
+                var em = document.createElement('em');
+                em.slot = 'someSlot';
+                host.appendChild(em);
+
+                var fakeEvent = new Event('slotchange');
+                fakeEvent.isFakeEvent = true;
+                slot.dispatchEvent(fakeEvent);
+            });
+
+            setTimeout(function () {
+                test.step(function () {
+                    assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed'
+                        + ' event if there was a synthetic slotchange event fired');
+                });
+                test.done();
+            }, 1);
+
+        }, 1);
+    }, 1);
+}
+
+testAppendingSpanToShadowRootWithNamedSlot('open', true);
+testAppendingSpanToShadowRootWithNamedSlot('closed', true);
+testAppendingSpanToShadowRootWithNamedSlot('open', false);
+testAppendingSpanToShadowRootWithNamedSlot('closed', false);
+
+function testSlotchangeDoesNotFireWhenOtherSlotsChange(mode, connectedToDocument)
+{
+    var test = async_test('slotchange event must not fire on a slot element inside '
+        + treeName(mode, connectedToDocument)
+        + ' when another slot\'s assigned nodes change');
+
+    var host;
+    var defaultSlotEventCount = 0;
+    var namedSlotEventCount = 0;
+
+    test.step(function () {
+        host = document.createElement('div');
+        if (connectedToDocument)
+            document.body.appendChild(host);
+
+        var shadowRoot = host.attachShadow({'mode': mode});
+        var defaultSlot = document.createElement('slot');
+        defaultSlot.addEventListener('slotchange', function (event) {
+            test.step(function () {
+                assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element');
+            });
+            defaultSlotEventCount++;
+        });
+
+        var namedSlot = document.createElement('slot');
+        namedSlot.name = 'slotName';
+        namedSlot.addEventListener('slotchange', function (event) {
+            test.step(function () {
+                assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element');
+            });
+            namedSlotEventCount++;
+        });
+
+        shadowRoot.appendChild(defaultSlot);
+        shadowRoot.appendChild(namedSlot);
+
+        host.appendChild(document.createElement('span'));
+
+        assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously');
+        assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously');
+    });
+
+    setTimeout(function () {
+        test.step(function () {
+            assert_equals(defaultSlotEventCount, 1,
+                'slotchange must be fired exactly once after the assigned nodes change on a default slot');
+            assert_equals(namedSlotEventCount, 0,
+                'slotchange must not be fired on a named slot after the assigned nodes change on a default slot');
+
+            var span = document.createElement('span');
+            span.slot = 'slotName';
+            host.appendChild(span);
+        });
+
+        setTimeout(function () {
+            test.step(function () {
+                assert_equals(defaultSlotEventCount, 1,
+                    'slotchange must not be fired on a default slot after the assigned nodes change on a named slot');
+                assert_equals(namedSlotEventCount, 1,
+                    'slotchange must be fired exactly once after the assigned nodes change on a default slot');
+            });
+            test.done();
+        }, 1);
+    }, 1);
+}
+
+testSlotchangeDoesNotFireWhenOtherSlotsChange('open', true);
+testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', true);
+testSlotchangeDoesNotFireWhenOtherSlotsChange('open', false);
+testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', false);
+
+function testSlotchangeDoesNotFireForMutationBeforeOrAfterSlotWasPresent(mode, connectedToDocument)
+{
+    var test = async_test('slotchange event must not fire on a slot element inside '
+        + treeName(mode, connectedToDocument)
+        + ' when the shadow host was mutated before the slot was inserted or after the slot was removed');
+
+    var host;
+    var slot;
+    var eventCount = 0;
+
+    test.step(function () {
+        host = document.createElement('div');
+        if (connectedToDocument)
+            document.body.appendChild(host);
+
+        var shadowRoot = host.attachShadow({'mode': mode});
+        slot = document.createElement('slot');
+        slot.addEventListener('slotchange', function (event) {
+            test.step(function () {
+                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
+            });
+            eventCount++;
+        });
+
+        host.appendChild(document.createElement('span'));
+        shadowRoot.appendChild(slot);
+
+        assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
+    });
+
+    setTimeout(function () {
+        test.step(function () {
+            assert_equals(eventCount, 0,
+                'slotchange must not be fired on a slot element if the assigned nodes changed before the slot was inserted');
+            host.removeChild(host.firstChild);
+        });
+
+        setTimeout(function () {
+            test.step(function () {
+                assert_equals(eventCount, 1,
+                    'slotchange must be fired exactly once after the assigned nodes change on a slot while the slot element was in the tree');
+                slot.parentNode.removeChild(slot);
+                host.appendChild(document.createElement('span'));
+            });
+
+            setTimeout(function () {
+                assert_equals(eventCount, 1,
+                    'slotchange must not be fired on a slot element if the assigned nodes changed after the slot was removed');
+                test.done();
+            }, 1);
+        }, 1);
+    }, 1);
+}
+
+testSlotchangeDoesNotFireForMutationBeforeOrAfterSlotWasPresent('open', true);
+testSlotchangeDoesNotFireForMutationBeforeOrAfterSlotWasPresent('closed', true);
+testSlotchangeDoesNotFireForMutationBeforeOrAfterSlotWasPresent('open', false);
+testSlotchangeDoesNotFireForMutationBeforeOrAfterSlotWasPresent('closed', false);
+
+function testSlotchangeFiresOnTransientlyPresentSlot(mode, connectedToDocument)
+{
+    var test = async_test('slotchange event must fire on a slot element inside '
+        + treeName(mode, connectedToDocument)
+        + ' even if the slot was removed immediately after the assigned nodes were mutated');
+
+    var host;
+    var slot;
+    var anotherSlot;
+    var slotEventCount = 0;
+    var anotherSlotEventCount = 0;
+
+    test.step(function () {
+        host = document.createElement('div');
+        if (connectedToDocument)
+            document.body.appendChild(host);
+
+        var shadowRoot = host.attachShadow({'mode': mode});
+        slot = document.createElement('slot');
+        slot.name = 'someSlot';
+        slot.addEventListener('slotchange', function (event) {
+            test.step(function () {
+                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
+            });
+            slotEventCount++;
+        });
+
+        anotherSlot = document.createElement('slot');
+        anotherSlot.name = 'someSlot';
+        anotherSlot.addEventListener('slotchange', function (event) {
+            test.step(function () {
+                assert_equals(event.target, anotherSlot, 'slotchange event\'s target must be the slot element');
+            });
+            anotherSlotEventCount++;
+        });
+
+        shadowRoot.appendChild(slot);
+
+        var span = document.createElement('span');
+        span.slot = 'someSlot';
+        host.appendChild(span);
+
+        shadowRoot.removeChild(slot);
+        shadowRoot.appendChild(anotherSlot);
+
+        var span = document.createElement('span');
+        span.slot = 'someSlot';
+        host.appendChild(span);
+
+        shadowRoot.removeChild(anotherSlot);
+
+        assert_equals(slotEventCount, 0, 'slotchange event must not be fired synchronously');
+        assert_equals(anotherSlotEventCount, 0, 'slotchange event must not be fired synchronously');
+    });
+
+    setTimeout(function () {
+        test.step(function () {
+            assert_equals(slotEventCount, 1,
+                'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present');
+            assert_equals(anotherSlotEventCount, 1,
+                'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present');
+        });
+        test.done();
+    }, 1);
+}
+
+testSlotchangeFiresOnTransientlyPresentSlot('open', true);
+testSlotchangeFiresOnTransientlyPresentSlot('closed', true);
+testSlotchangeFiresOnTransientlyPresentSlot('open', false);
+testSlotchangeFiresOnTransientlyPresentSlot('closed', false);
+
+function testSlotchangeFiresOnInnerHTML(mode, connectedToDocument)
+{
+    var test = async_test('slotchange event must fire on a slot element inside '
+        + treeName(mode, connectedToDocument)
+        + ' when innerHTML modifies the children of the shadow host');
+
+    var host;
+    var defaultSlot;
+    var namedSlot;
+    var defaultSlotEventCount = 0;
+    var namedSlotEventCount = 0;
+
+    test.step(function () {
+        host = document.createElement('div');
+        if (connectedToDocument)
+            document.body.appendChild(host);
+
+        var shadowRoot = host.attachShadow({'mode': mode});
+        defaultSlot = document.createElement('slot');
+        defaultSlot.addEventListener('slotchange', function (event) {
+            test.step(function () {
+                assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element');
+            });
+            defaultSlotEventCount++;
+        });
+
+        namedSlot = document.createElement('slot');
+        namedSlot.name = 'someSlot';
+        namedSlot.addEventListener('slotchange', function (event) {
+            test.step(function () {
+                assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element');
+            });
+            namedSlotEventCount++;
+        });
+        shadowRoot.appendChild(namedSlot);
+        shadowRoot.appendChild(defaultSlot);
+        host.innerHTML = 'foo <b>bar</b>';
+
+        assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously');
+        assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously');
+    });
+
+    setTimeout(function () {
+        test.step(function () {
+            assert_equals(defaultSlotEventCount, 1,
+                'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
+            assert_equals(namedSlotEventCount, 0,
+                'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
+            host.innerHTML = 'baz';
+        });
+        setTimeout(function () {
+            test.step(function () {
+                assert_equals(defaultSlotEventCount, 2,
+                    'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
+                assert_equals(namedSlotEventCount, 0,
+                    'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
+                host.innerHTML = '';
+            });
+            setTimeout(function () {
+                test.step(function () {
+                    assert_equals(defaultSlotEventCount, 3,
+                        'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
+                    assert_equals(namedSlotEventCount, 0,
+                        'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
+                    host.innerHTML = '<b slot="someSlot">content</b>';
+                });
+                setTimeout(function () {
+                    test.step(function () {
+                        assert_equals(defaultSlotEventCount, 3,
+                            'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
+                        assert_equals(namedSlotEventCount, 1,
+                            'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML');
+                        host.innerHTML = '';
+                    });
+                    setTimeout(function () {
+                        test.step(function () {
+                            // FIXME: This test would fail in the current implementation because we can't tell
+                            // whether a text node was removed in AllChildrenRemoved or not.
+//                            assert_equals(defaultSlotEventCount, 3,
+//                                'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
+                            assert_equals(namedSlotEventCount, 2,
+                                'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML');
+                        });
+                        test.done();
+                    }, 1);
+                }, 1);
+            }, 1);
+        }, 1);
+    }, 1);
+}
+
+testSlotchangeFiresOnInnerHTML('open', true);
+testSlotchangeFiresOnInnerHTML('closed', true);
+testSlotchangeFiresOnInnerHTML('open', false);
+testSlotchangeFiresOnInnerHTML('closed', false);
+
+function testSlotchangeFiresWhenNestedSlotChange(mode, connectedToDocument)
+{
+    var test = async_test('slotchange event must fire on a slot element inside '
+        + treeName(mode, connectedToDocument)
+        + ' when nested slots\'s contents change');
+
+    var outerHost;
+    var innerHost;
+    var outerSlot;
+    var innerSlot;
+    var outerSlotEventCount = 0;
+    var innerSlotEventCount = 0;
+
+    test.step(function () {
+        outerHost = document.createElement('div');
+        if (connectedToDocument)
+            document.body.appendChild(outerHost);
+
+        var outerShadow = outerHost.attachShadow({'mode': mode});
+        outerShadow.appendChild(document.createElement('span'));
+        outerSlot = document.createElement('slot');
+        outerSlot.addEventListener('slotchange', function (event) {
+            event.stopPropagation();
+            test.step(function () {
+                assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the slot element');
+            });
+            outerSlotEventCount++;
+        });
+
+        innerHost = document.createElement('div');
+        innerHost.appendChild(outerSlot);
+        outerShadow.appendChild(innerHost);
+
+        var innerShadow = innerHost.attachShadow({'mode': mode});
+        innerShadow.appendChild(document.createElement('span'));
+        innerSlot = document.createElement('slot');
+        innerSlot.addEventListener('slotchange', function (event) {
+            event.stopPropagation();
+            test.step(function () {
+                assert_equals(event.target, innerSlot, 'slotchange event\'s target must be the slot element');
+            });
+            innerSlotEventCount++;
+        });
+        innerShadow.appendChild(innerSlot);
+
+        outerHost.appendChild(document.createElement('span'));
+
+        assert_equals(innerSlotEventCount, 0, 'slotchange event must not be fired synchronously');
+        assert_equals(outerSlotEventCount, 0, 'slotchange event must not be fired synchronously');
+    });
+
+    setTimeout(function () {
+        test.step(function () {
+            assert_equals(innerSlotEventCount, 1,
+                'slotchange must be fired on a slot element if the assigned nodes changed');
+            assert_equals(outerSlotEventCount, 1,
+                'slotchange must be fired on a slot element if the assigned nodes of an inner slot changed');
+        });
+        test.done();
+    }, 1);
+}
+
+testSlotchangeFiresWhenNestedSlotChange('open', true);
+testSlotchangeFiresWhenNestedSlotChange('closed', true);
+testSlotchangeFiresWhenNestedSlotChange('open', false);
+testSlotchangeFiresWhenNestedSlotChange('closed', false);
+
+</script>
+</body>
+</html>
index 6a89fe1..6996bfe 100644 (file)
@@ -1,3 +1,97 @@
+2016-03-14  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Add slotchange event
+        https://bugs.webkit.org/show_bug.cgi?id=155424
+        <rdar://problem/24997534>
+
+        Reviewed by Antti Koivisto.
+
+        Added `slotchange` event as discussed on https://github.com/w3c/webcomponents/issues/288.
+
+        While the exact semantics of it could still evolve over time, this patch implements as
+        an asynchronous event that fires on a slot element whenever its distributed nodes change
+        (flattened assigned nodes):
+        http://w3c.github.io/webcomponents/spec/shadow/#dfn-distributed-nodes
+
+        Since inserting or removing an element from a shadow host could needs to enqueue this event
+        on the right slot element, this patch moves the invalidation point of element removals and
+        insertions from Element::childrenChanged to Element::insertedInto and Element::removedFrom.
+        Text nodes are still invalidated at Element::childrenChanged for performance reasons
+        since it could only appear within a default slot element.
+
+        Because this more fine-grained invalidation needs to be overridden by HTMLDetailsElement,
+        we now subclass SlotAssignment in HTMLDetailsElement instead of passing in a std::function.
+
+        Test: fast/shadow-dom/slotchange-event.html
+
+        * dom/Document.cpp:
+        (WebCore::Document::enqueueSlotchangeEvent): Added.
+        * dom/Document.h:
+        * dom/Element.cpp:
+        (WebCore::Element::attributeChanged): Call hostChildElementDidChangeSlotAttr.
+        (WebCore::Element::insertedInto): Call hostChildElementDidChange.
+        (WebCore::Element::removedFrom): Ditto.
+        (WebCore::Element::childrenChanged): Don't invalidate the slots on ElementInserted and
+        ElementRemoved since they're now done in Element::insertedInto and Element::removedFrom.
+        * dom/Event.cpp:
+        (WebCore::Event::scoped): slotchange event is scoped.
+        * dom/EventNames.h: Added eventNames().slotchange.
+        * dom/ShadowRoot.cpp:
+        (WebCore::ShadowRoot::invalidateSlotAssignments): Deleted.
+        (WebCore::ShadowRoot::invalidateDefaultSlotAssignments): Deleted.
+        * dom/ShadowRoot.h:
+        (ShadowRoot): Added more fine-grained invalidators, mirroring changes to SlotAssignment.
+        * dom/SlotAssignment.cpp:
+        (WebCore::SlotAssignment::SlotAssignment): Removed a variant that takes SlotNameFunction
+        since HTMLDetailsElement now subclasses SlotAssignment.
+        (WebCore::SlotAssignment::~SlotAssignment): Added now that the class is virtual.
+        (WebCore::recursivelyFireSlotChangeEvent): Added.
+        (WebCore::SlotAssignment::didChangeSlot): Added. Invalidates the style tree only if there
+        is a corresponding slot element, and fires slotchange event. When the slot element we found
+        in this shadow tree is assigned to a slot element inside an inner shadow tree, recursively
+        fire slotchange event on each such inner slots.
+        (WebCore::SlotAssignment::hostChildElementDidChange): Added. Update the matching slot when
+        an element is inserted or removed under a shadow host.
+        (WebCore::SlotAssignment::assignedNodesForSlot): Removed the superfluous early exit to an
+        release assert since addSlotElementByName should always create a SlotInfo for each element.
+        (WebCore::SlotAssignment::slotNameForHostChild): Added. This is the equivalent of old
+        m_slotNameFunction which DetailsSlotAssignment overrides.
+        (WebCore::SlotAssignment::invalidateDefaultSlot): Deleted.
+        (WebCore::SlotAssignment::findFirstSlotElement): Added an assertion. slotInfo.element must
+        be nullptr if elementCount is 0, and elementCount must be 0 if slotInfo.element is nullptr
+        after calling resolveAllSlotElements, which traverses the entire shadow tree to find all
+        slot elements.
+        (WebCore::SlotAssignment::assignSlots):
+        * dom/SlotAssignment.h: Implemented inline functions of ShadowRoot here to avoid including
+        SlotAssignment.h in ShadowRoot.h. Not inlining them results in extra function calls for all
+        builtin elements with shadow root without slot elements, which impacts performance.
+        (WebCore::ShadowRoot::didRemoveAllChildrenOfShadowHost): Added.
+        (WebCore::ShadowRoot::didChangeDefaultSlot): Added.
+        (WebCore::ShadowRoot::hostChildElementDidChange): Added.
+        (WebCore::ShadowRoot::hostChildElementDidChangeSlotAttribute): Added.
+        (WebCore::ShadowRoot::innerSlotDidChange):
+        * html/HTMLDetailsElement.cpp:
+        (WebCore::DetailsSlotAssignment): Added. Subclasses SlotAssignment to override
+        hostChildElementDidChange and slotNameForHostChild.
+        (WebCore::DetailsSlotAssignment::hostChildElementDidChange): Added. We don't check if this
+        is the first summary element since we don't know the answer when this function is called
+        inside Element::removedFrom.
+        (WebCore::DetailsSlotAssignment::slotNameForHostChild): Renamed from slotNameFunction. Also
+        removed the code to return nullAtom when details element is not open as that messes up new
+        fine-grained invalidation. Insert/remove the slot element in parseAttribute instead.
+        (WebCore::HTMLDetailsElement::didAddUserAgentShadowRoot): Don't insert the slot element for
+        the summary since the details element is not open now.
+        (WebCore::HTMLDetailsElement::parseAttribute): Remove and insert the slot element for the
+        summary here instead of changing the behavior of slotNameForHostChild.
+        * html/HTMLDetailsElement.h:
+        * html/HTMLSlotElement.cpp:
+        (WebCore::HTMLSlotElement::enqueueSlotChangeEvent): Added. Enqueues a new slotchange event
+        if we haven't done so for this element yet.
+        (WebCore::HTMLSlotElement::dispatchEvent): Added. Clear m_hasEnqueuedSlotChangeEvent when
+        dispatching a slotchange event so that a subsequent call to enqueueSlotChangeEvent would
+        enqueue a new event. Note scripts call EventTarget::dispatchEventForBindings instead.
+        * html/HTMLSlotElement.h:
+
 2016-03-14  Youenn Fablet  <youenn.fablet@crf.canon.fr>
 
         Introduce CallWith=Document in binding generator
index 4ec2f41..8cd7989 100644 (file)
@@ -4172,6 +4172,11 @@ void Document::enqueueOverflowEvent(Ref<Event>&& event)
     m_eventQueue.enqueueEvent(WTFMove(event));
 }
 
+void Document::enqueueSlotchangeEvent(Ref<Event>&& event)
+{
+    m_eventQueue.enqueueEvent(WTFMove(event));
+}
+
 RefPtr<Event> Document::createEvent(const String& type, ExceptionCode& ec)
 {
     // Please do *not* add new event classes to this function unless they are
index 59156db..d180a90 100644 (file)
@@ -1088,6 +1088,7 @@ public:
     void enqueueWindowEvent(Ref<Event>&&);
     void enqueueDocumentEvent(Ref<Event>&&);
     void enqueueOverflowEvent(Ref<Event>&&);
+    void enqueueSlotchangeEvent(Ref<Event>&&);
     void enqueuePageshowEvent(PageshowEventPersistence);
     void enqueueHashchangeEvent(const String& oldURL, const String& newURL);
     void enqueuePopstateEvent(RefPtr<SerializedScriptValue>&& stateObject);
index 82b2406..f9399d8 100644 (file)
@@ -83,6 +83,7 @@
 #include "SelectorQuery.h"
 #include "Settings.h"
 #include "SimulatedClick.h"
+#include "SlotAssignment.h"
 #include "StyleProperties.h"
 #include "StyleResolver.h"
 #include "StyleTreeResolver.h"
@@ -1254,7 +1255,7 @@ void Element::attributeChanged(const QualifiedName& name, const AtomicString& ol
         else if (name == HTMLNames::slotAttr) {
             if (auto* parent = parentElement()) {
                 if (auto* shadowRoot = parent->shadowRoot())
-                    shadowRoot->invalidateSlotAssignments();
+                    shadowRoot->hostChildElementDidChangeSlotAttribute(oldValue, newValue);
             }
         }
 #endif
@@ -1496,6 +1497,11 @@ Node::InsertionNotificationRequest Element::insertedInto(ContainerNode& insertio
         setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(true);
 #endif
 
+    if (parentNode() == &insertionPoint) {
+        if (auto* shadowRoot = parentNode()->shadowRoot())
+            shadowRoot->hostChildElementDidChange(*this);
+    }
+
     if (!insertionPoint.isInTreeScope())
         return InsertionDone;
 
@@ -1575,6 +1581,11 @@ void Element::removedFrom(ContainerNode& insertionPoint)
         }
     }
 
+    if (!parentNode()) {
+        if (auto* shadowRoot = insertionPoint.shadowRoot())
+            shadowRoot->hostChildElementDidChange(*this);
+    }
+
     ContainerNode::removedFrom(insertionPoint);
 
     if (hasPendingResources())
@@ -1832,13 +1843,15 @@ void Element::childrenChanged(const ChildChange& change)
         switch (change.type) {
         case ElementInserted:
         case ElementRemoved:
+            // For elements, we notify shadowRoot in Element::insertedInto and Element::removedFrom.
+            break;
         case AllChildrenRemoved:
-            shadowRoot->invalidateSlotAssignments();
+            shadowRoot->didRemoveAllChildrenOfShadowHost();
             break;
         case TextInserted:
         case TextRemoved:
         case TextChanged:
-            shadowRoot->invalidateDefaultSlotAssignments();
+            shadowRoot->didChangeDefaultSlot();
             break;
         case NonContentsChildChanged:
             break;
index e87cf1a..949f4ab 100644 (file)
@@ -103,7 +103,8 @@ bool Event::scoped() const
         || m_type == eventNames().resizeEvent
         || m_type == eventNames().scrollEvent
         || m_type == eventNames().selectEvent
-        || m_type == eventNames().selectstartEvent;
+        || m_type == eventNames().selectstartEvent
+        || m_type == eventNames().slotchangeEvent;
 }
 
 EventInterface Event::eventInterface() const
index d24b9b9..4ce8678 100644 (file)
@@ -195,6 +195,7 @@ namespace WebCore {
     macro(selectstart) \
     macro(show) \
     macro(signalingstatechange) \
+    macro(slotchange) \
     macro(soundend) \
     macro(soundstart) \
     macro(sourceclose) \
index c6f7212..b7758b8 100644 (file)
@@ -201,18 +201,6 @@ void ShadowRoot::removeSlotElementByName(const AtomicString& name, HTMLSlotEleme
     return m_slotAssignment->removeSlotElementByName(name, slot, *this);
 }
 
-void ShadowRoot::invalidateSlotAssignments()
-{
-    if (m_slotAssignment)
-        m_slotAssignment->invalidate(*this);
-}
-
-void ShadowRoot::invalidateDefaultSlotAssignments()
-{
-    if (m_slotAssignment)
-        m_slotAssignment->invalidateDefaultSlot(*this);
-}
-
 const Vector<Node*>* ShadowRoot::assignedNodesForSlot(const HTMLSlotElement& slot)
 {
     if (!m_slotAssignment)
index 692887d..db60ef0 100644 (file)
@@ -91,8 +91,11 @@ public:
     void addSlotElementByName(const AtomicString&, HTMLSlotElement&);
     void removeSlotElementByName(const AtomicString&, HTMLSlotElement&);
 
-    void invalidateSlotAssignments();
-    void invalidateDefaultSlotAssignments();
+    void didRemoveAllChildrenOfShadowHost();
+    void didChangeDefaultSlot();
+    void hostChildElementDidChange(const Element&);
+    void hostChildElementDidChangeSlotAttribute(const AtomicString& oldValue, const AtomicString& newValue);
+    void innerSlotDidChange(const AtomicString&);
 
     const Vector<Node*>* assignedNodesForSlot(const HTMLSlotElement&);
 #endif
index 9171849..2b4093c 100644 (file)
@@ -50,12 +50,10 @@ static const AtomicString& slotNameFromSlotAttribute(const Node& child)
 }
 
 SlotAssignment::SlotAssignment()
-    : m_slotNameFunction(slotNameFromSlotAttribute)
 {
 }
 
-SlotAssignment::SlotAssignment(SlotNameFunction function)
-    : m_slotNameFunction(WTFMove(function))
+SlotAssignment::~SlotAssignment()
 {
 }
 
@@ -64,10 +62,7 @@ HTMLSlotElement* SlotAssignment::findAssignedSlot(const Node& node, ShadowRoot&
     if (!is<Text>(node) && !is<Element>(node))
         return nullptr;
 
-    auto slotName = m_slotNameFunction(node);
-    if (!slotName)
-        return nullptr;
-
+    auto slotName = slotNameForHostChild(node);
     auto it = m_slots.find(slotName);
     if (it == m_slots.end())
         return nullptr;
@@ -133,17 +128,58 @@ void SlotAssignment::removeSlotElementByName(const AtomicString& name, HTMLSlotE
     ASSERT(slotInfo.element || m_needsToResolveSlotElements);
 }
 
-const Vector<Node*>* SlotAssignment::assignedNodesForSlot(const HTMLSlotElement& slotElement, ShadowRoot& shadowRoot)
+static void recursivelyFireSlotChangeEvent(HTMLSlotElement& slotElement)
 {
-    if (!m_slotAssignmentsIsValid)
-        assignSlots(shadowRoot);
+    slotElement.enqueueSlotChangeEvent();
 
-    const AtomicString& slotName = slotNameFromAttributeValue(slotElement.fastGetAttribute(nameAttr));
+    auto* slotParent = slotElement.parentElement();
+    if (!slotParent)
+        return;
+
+    auto* shadowRootOfSlotParent = slotParent->shadowRoot();
+    if (!shadowRootOfSlotParent)
+        return;
+
+    shadowRootOfSlotParent->innerSlotDidChange(slotElement.fastGetAttribute(slotAttr));
+}
+
+void SlotAssignment::didChangeSlot(const AtomicString& slotAttrValue, ChangeType changeType, ShadowRoot& shadowRoot)
+{
+    auto& slotName = slotNameFromAttributeValue(slotAttrValue);
     auto it = m_slots.find(slotName);
     if (it == m_slots.end())
-        return nullptr;
+        return;
+
+    HTMLSlotElement* slotElement = findFirstSlotElement(*it->value, shadowRoot);
+    if (!slotElement)
+        return;
+
+    if (changeType == ChangeType::DirectChild) {
+        shadowRoot.host()->setNeedsStyleRecalc(ReconstructRenderTree);
+        m_slotAssignmentsIsValid = false;
+    }
+
+    if (shadowRoot.type() == ShadowRoot::Type::UserAgent)
+        return;
+
+    recursivelyFireSlotChangeEvent(*slotElement);
+}
+
+void SlotAssignment::hostChildElementDidChange(const Element& childElement, ShadowRoot& shadowRoot)
+{
+    didChangeSlot(childElement.fastGetAttribute(slotAttr), ChangeType::DirectChild, shadowRoot);
+}
+
+const Vector<Node*>* SlotAssignment::assignedNodesForSlot(const HTMLSlotElement& slotElement, ShadowRoot& shadowRoot)
+{
+    ASSERT(slotElement.containingShadowRoot() == &shadowRoot);
+    const AtomicString& slotName = slotNameFromAttributeValue(slotElement.fastGetAttribute(nameAttr));
+    auto it = m_slots.find(slotName);
+    RELEASE_ASSERT(it != m_slots.end());
 
     auto& slotInfo = *it->value;
+    if (!m_slotAssignmentsIsValid)
+        assignSlots(shadowRoot);
 
     if (!slotInfo.assignedNodes.size())
         return nullptr;
@@ -155,18 +191,9 @@ const Vector<Node*>* SlotAssignment::assignedNodesForSlot(const HTMLSlotElement&
     return &slotInfo.assignedNodes;
 }
 
-void SlotAssignment::invalidate(ShadowRoot& shadowRoot)
-{
-    // FIXME: We should be able to do a targeted reconstruction.
-    shadowRoot.host()->setNeedsStyleRecalc(ReconstructRenderTree);
-    m_slotAssignmentsIsValid = false;
-}
-
-void SlotAssignment::invalidateDefaultSlot(ShadowRoot& shadowRoot)
+const AtomicString& SlotAssignment::slotNameForHostChild(const Node& child) const
 {
-    auto it = m_slots.find(defaultSlotName());
-    if (it != m_slots.end() && it->value->elementCount)
-        invalidate(shadowRoot); // FIXME: We should be able to reconstruct only under the default slot.
+    return slotNameFromSlotAttribute(child);
 }
 
 HTMLSlotElement* SlotAssignment::findFirstSlotElement(SlotInfo& slotInfo, ShadowRoot& shadowRoot)
@@ -176,6 +203,7 @@ HTMLSlotElement* SlotAssignment::findFirstSlotElement(SlotInfo& slotInfo, Shadow
 
 #ifndef NDEBUG
     ASSERT(!slotInfo.element || m_slotElementsForConsistencyCheck.contains(slotInfo.element));
+    ASSERT(!!slotInfo.element == !!slotInfo.elementCount);
 #endif
 
     return slotInfo.element;
@@ -223,9 +251,7 @@ void SlotAssignment::assignSlots(ShadowRoot& shadowRoot)
     for (auto* child = host.firstChild(); child; child = child->nextSibling()) {
         if (!is<Text>(*child) && !is<Element>(*child))
             continue;
-        auto slotName = m_slotNameFunction(*child);
-        if (!slotName)
-            continue;
+        auto slotName = slotNameForHostChild(*child);
         assignToSlot(*child, slotName);
     }
 
index 0fcd4df..4e2bc60 100644 (file)
@@ -28,6 +28,7 @@
 
 #if ENABLE(SHADOW_DOM) || ENABLE(DETAILS_ELEMENT)
 
+#include "ShadowRoot.h"
 #include <wtf/HashMap.h>
 #include <wtf/HashSet.h>
 #include <wtf/Vector.h>
 
 namespace WebCore {
 
+class Element;
 class HTMLSlotElement;
 class Node;
-class ShadowRoot;
 
 class SlotAssignment {
     WTF_MAKE_NONCOPYABLE(SlotAssignment);
 public:
-    using SlotNameFunction = std::function<AtomicString (const Node& child)>;
-
     SlotAssignment();
-    SlotAssignment(SlotNameFunction);
-    ~SlotAssignment() { }
+    virtual ~SlotAssignment();
 
     static const AtomicString& defaultSlotName() { return emptyAtom; }
 
@@ -56,10 +54,13 @@ public:
     void addSlotElementByName(const AtomicString&, HTMLSlotElement&, ShadowRoot&);
     void removeSlotElementByName(const AtomicString&, HTMLSlotElement&, ShadowRoot&);
 
+    enum class ChangeType { DirectChild, InnerSlot };
+    void didChangeSlot(const AtomicString&, ChangeType, ShadowRoot&);
+    void enqueueSlotChangeEvent(const AtomicString&, ShadowRoot&);
+
     const Vector<Node*>* assignedNodesForSlot(const HTMLSlotElement&, ShadowRoot&);
 
-    void invalidate(ShadowRoot&);
-    void invalidateDefaultSlot(ShadowRoot&);
+    virtual void hostChildElementDidChange(const Element&, ShadowRoot&);
 
 private:
     struct SlotInfo {
@@ -77,6 +78,8 @@ private:
         unsigned elementCount { 0 };
         Vector<Node*> assignedNodes;
     };
+    
+    virtual const AtomicString& slotNameForHostChild(const Node&) const;
 
     HTMLSlotElement* findFirstSlotElement(SlotInfo&, ShadowRoot&);
     void resolveAllSlotElements(ShadowRoot&);
@@ -84,8 +87,6 @@ private:
     void assignSlots(ShadowRoot&);
     void assignToSlot(Node& child, const AtomicString& slotName);
 
-    SlotNameFunction m_slotNameFunction;
-
     HashMap<AtomicString, std::unique_ptr<SlotInfo>> m_slots;
 
 #ifndef NDEBUG
@@ -96,6 +97,38 @@ private:
     bool m_slotAssignmentsIsValid { false };
 };
 
+inline void ShadowRoot::didRemoveAllChildrenOfShadowHost()
+{
+    if (m_slotAssignment) // FIXME: This is incorrect when there were no elements or text nodes removed.
+        m_slotAssignment->didChangeSlot(nullAtom, SlotAssignment::ChangeType::DirectChild, *this);
+}
+
+inline void ShadowRoot::didChangeDefaultSlot()
+{
+    if (m_slotAssignment)
+        m_slotAssignment->didChangeSlot(nullAtom, SlotAssignment::ChangeType::DirectChild, *this);
+}
+
+inline void ShadowRoot::hostChildElementDidChange(const Element& childElement)
+{
+    if (m_slotAssignment)
+        m_slotAssignment->hostChildElementDidChange(childElement, *this);
+}
+
+inline void ShadowRoot::hostChildElementDidChangeSlotAttribute(const AtomicString& oldValue, const AtomicString& newValue)
+{
+    if (m_slotAssignment) {
+        m_slotAssignment->didChangeSlot(oldValue, SlotAssignment::ChangeType::DirectChild, *this);
+        m_slotAssignment->didChangeSlot(newValue, SlotAssignment::ChangeType::DirectChild, *this);
+    }
+}
+
+inline void ShadowRoot::innerSlotDidChange(const AtomicString& name)
+{
+    if (m_slotAssignment)
+        m_slotAssignment->didChangeSlot(name, SlotAssignment::ChangeType::InnerSlot, *this);
+}
+
 }
 
 #endif
index e8bdf30..b4cb0c8 100644 (file)
@@ -44,7 +44,23 @@ static const AtomicString& summarySlotName()
     return summarySlot;
 }
 
-static AtomicString slotNameFunction(const Node& child)
+class DetailsSlotAssignment final : public SlotAssignment {
+private:
+    void hostChildElementDidChange(const Element&, ShadowRoot&) override;
+    const AtomicString& slotNameForHostChild(const Node&) const override;
+};
+
+void DetailsSlotAssignment::hostChildElementDidChange(const Element& childElement, ShadowRoot& shadowRoot)
+{
+    if (is<HTMLSummaryElement>(childElement)) {
+        // Don't check whether this is the first summary element
+        // since we don't know the answer when this function is called inside Element::removedFrom.
+        didChangeSlot(summarySlotName(), ChangeType::DirectChild, shadowRoot);
+    } else
+        didChangeSlot(SlotAssignment::defaultSlotName(), ChangeType::DirectChild, shadowRoot);
+}
+
+const AtomicString& DetailsSlotAssignment::slotNameForHostChild(const Node& child) const
 {
     auto& parent = *child.parentNode();
     ASSERT(is<HTMLDetailsElement>(parent));
@@ -55,18 +71,13 @@ static AtomicString slotNameFunction(const Node& child)
         if (&child == childrenOfType<HTMLSummaryElement>(details).first())
             return summarySlotName();
     }
-    // Everything else is assigned to the default slot if details is open.
-    if (details.isOpen())
-        return SlotAssignment::defaultSlotName();
-
-    // Otherwise don't render the content.
-    return nullAtom;
-};
+    return SlotAssignment::defaultSlotName();
+}
 
 Ref<HTMLDetailsElement> HTMLDetailsElement::create(const QualifiedName& tagName, Document& document)
 {
     auto details = adoptRef(*new HTMLDetailsElement(tagName, document));
-    details->addShadowRoot(ShadowRoot::create(document, std::make_unique<SlotAssignment>(slotNameFunction)));
+    details->addShadowRoot(ShadowRoot::create(document, std::make_unique<DetailsSlotAssignment>()));
     return details;
 }
 
@@ -94,8 +105,8 @@ void HTMLDetailsElement::didAddUserAgentShadowRoot(ShadowRoot* root)
     summarySlot->appendChild(WTFMove(defaultSummary));
     root->appendChild(WTFMove(summarySlot));
 
-    auto defaultSlot = HTMLSlotElement::create(slotTag, document());
-    root->appendChild(WTFMove(defaultSlot));
+    m_defaultSlot = HTMLSlotElement::create(slotTag, document());
+    ASSERT(!m_isOpen);
 }
 
 bool HTMLDetailsElement::isActiveSummary(const HTMLSummaryElement& summary) const
@@ -117,8 +128,14 @@ void HTMLDetailsElement::parseAttribute(const QualifiedName& name, const AtomicS
     if (name == openAttr) {
         bool oldValue = m_isOpen;
         m_isOpen = !value.isNull();
-        if (oldValue != m_isOpen)
-            shadowRoot()->invalidateSlotAssignments();
+        if (oldValue != m_isOpen) {
+            auto* root = shadowRoot();
+            ASSERT(root);
+            if (m_isOpen)
+                root->appendChild(*m_defaultSlot);
+            else
+                root->removeChild(*m_defaultSlot);
+        }
     } else
         HTMLElement::parseAttribute(name, value);
 }
index bf549b6..5f6f9ad 100644 (file)
@@ -47,6 +47,7 @@ private:
     bool m_isOpen { false };
     HTMLSlotElement* m_summarySlot { nullptr };
     HTMLSummaryElement* m_defaultSummary { nullptr };
+    RefPtr<HTMLSlotElement> m_defaultSlot;
 };
 
 } // namespace WebCore
index 73cc961..7a23778 100644 (file)
@@ -29,6 +29,8 @@
 #if ENABLE(SHADOW_DOM) || ENABLE(DETAILS_ELEMENT)
 
 #include "ElementChildIterator.h"
+#include "Event.h"
+#include "EventNames.h"
 #include "HTMLNames.h"
 #include "ShadowRoot.h"
 
@@ -97,6 +99,27 @@ const Vector<Node*>* HTMLSlotElement::assignedNodes() const
     return shadowRoot->assignedNodesForSlot(*this);
 }
 
+void HTMLSlotElement::enqueueSlotChangeEvent()
+{
+    if (m_hasEnqueuedSlotChangeEvent)
+        return;
+
+    bool bubbles = false;
+    bool cancelable = false;
+    auto event = Event::create(eventNames().slotchangeEvent, bubbles, cancelable);
+    event->setTarget(this);
+    document().enqueueSlotchangeEvent(WTFMove(event));
+
+    m_hasEnqueuedSlotChangeEvent = true;
+}
+
+bool HTMLSlotElement::dispatchEvent(Event& event)
+{
+    if (event.type() == eventNames().slotchangeEvent)
+        m_hasEnqueuedSlotChangeEvent = false;
+    return HTMLElement::dispatchEvent(event);
+}
+
 }
 
 #endif
index ab161ff..8a4484e 100644 (file)
@@ -39,12 +39,18 @@ public:
 
     const Vector<Node*>* assignedNodes() const;
 
+    void enqueueSlotChangeEvent();
+
 private:
     HTMLSlotElement(const QualifiedName&, Document&);
 
     InsertionNotificationRequest insertedInto(ContainerNode&) override;
     void removedFrom(ContainerNode&) override;
     void attributeChanged(const QualifiedName&, const AtomicString& oldValue, const AtomicString& newValue, AttributeModificationReason) override;
+
+    bool dispatchEvent(Event&) override;
+
+    bool m_hasEnqueuedSlotChangeEvent { false };
 };
 
 }