Add the support for upgrading custom elements in cloneNode
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 6 Mar 2016 05:07:47 +0000 (05:07 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 6 Mar 2016 05:07:47 +0000 (05:07 +0000)
https://bugs.webkit.org/show_bug.cgi?id=155062

Reviewed by Antti Koivisto.

Source/WebCore:

Implement https://w3c.github.io/webcomponents/spec/custom/#upgrading and steps 6 through 11 in
https://w3c.github.io/webcomponents/spec/custom/#htmlelement-constructor to support upgrading elements
created by Node.prototype.cloneNode.

Tests: fast/custom-elements/lifecycle-callback-timing.html
       fast/custom-elements/upgrading/Node-cloneNode.html

* bindings/js/JSCustomElementInterface.cpp:
(WebCore::JSCustomElementInterface::upgradeElement): Added. Implements
 https://w3c.github.io/webcomponents/spec/custom/#dfn-upgrade-a-custom-element
(WebCore::JSCustomElementInterface::didUpgradeLastElementInConstructionStack): Added. Implements step 10
 "Replace the last entry in definition's construction stacka with an already constructed marker."
 in https://w3c.github.io/webcomponents/spec/custom/#dom-htmlelement-constructor
* bindings/js/JSCustomElementInterface.h:
(WebCore::JSCustomElementInterface::isUpgradingElement):
(WebCore::JSCustomElementInterface::lastElementInConstructionStack):
(WebCore::JSCustomElementInterface): Added m_constructionStack. This is the construction stack:
 https://w3c.github.io/webcomponents/spec/custom/#dfn-element-definition-construction-stack
* bindings/js/JSDOMBinding.cpp:
(WebCore::throwInvalidStateError): Added.
* bindings/js/JSDOMBinding.h:
* bindings/js/JSHTMLElementCustom.cpp:
(WebCore::constructJSHTMLElement): Implement the upgrading case in:
 https://w3c.github.io/webcomponents/spec/custom/#htmlelement-constructor
* dom/Document.cpp:
(WebCore::createFallbackHTMLElement): Added. Enqueues upgrades of custom elements (enqueueElementUpgrade
 currently does nothing if there is no InvokesCustomElementLifecycleCallbacks; e.g. in other DOM APIs).
 This function implements https://w3c.github.io/webcomponents/spec/custom/#dfn-element-upgrade-algorithm
(WebCore::Document::createElement):
* dom/LifecycleCallbackQueue.cpp:
(WebCore::LifecycleQueueItem::LifecycleQueueItem): Added a generic constructor.
(WebCore::LifecycleQueueItem::invoke): Call upgradeElement when m_type is Type::ElementUpgrade.
(WebCore::LifecycleCallbackQueue::enqueueElementUpgrade): Added.
* dom/LifecycleCallbackQueue.h:
* dom/Node.idl: Added InvokesCustomElementLifecycleCallbacks on cloneNode.
* dom/make_names.pl:
(printFactoryCppFile): Added a variant of createKnownElement which takes QualifiedName. Also directly call
 find(HTML|SVG|MathML)ElementConstructorFunction in createElement that takes AtomicString to avoid an extra
 function call.
(printFactoryHeaderFile): Added a function declaration for createKnownElement that takes QualifiedName and
 outdented class and function declarations to match the modern code style guideline.

LayoutTests:

Added test cases for upgrading elements with Node.prototype.cloneNode.

* fast/custom-elements/lifecycle-callback-timing-expected.txt:
* fast/custom-elements/lifecycle-callback-timing.html:

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

16 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/custom-elements/lifecycle-callback-timing-expected.txt
LayoutTests/fast/custom-elements/lifecycle-callback-timing.html
LayoutTests/fast/custom-elements/upgrading/Node-cloneNode-expected.txt [new file with mode: 0644]
LayoutTests/fast/custom-elements/upgrading/Node-cloneNode.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/bindings/js/JSCustomElementInterface.cpp
Source/WebCore/bindings/js/JSCustomElementInterface.h
Source/WebCore/bindings/js/JSDOMBinding.cpp
Source/WebCore/bindings/js/JSDOMBinding.h
Source/WebCore/bindings/js/JSHTMLElementCustom.cpp
Source/WebCore/dom/Document.cpp
Source/WebCore/dom/LifecycleCallbackQueue.cpp
Source/WebCore/dom/LifecycleCallbackQueue.h
Source/WebCore/dom/Node.idl
Source/WebCore/dom/make_names.pl

index c05496c..7f76a6a 100644 (file)
@@ -1,3 +1,15 @@
+2016-03-05  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Add the support for upgrading custom elements in cloneNode
+        https://bugs.webkit.org/show_bug.cgi?id=155062
+
+        Reviewed by Antti Koivisto.
+
+        Added test cases for upgrading elements with Node.prototype.cloneNode.
+
+        * fast/custom-elements/lifecycle-callback-timing-expected.txt:
+        * fast/custom-elements/lifecycle-callback-timing.html:
+
 2016-03-05  Sam Weinig  <sam@webkit.org>
 
         Update tests to match the latest version of the HTML5 spec.
index 19ef673..9f03047 100644 (file)
@@ -1,3 +1,4 @@
 
 PASS setAttribute and removeAttribute must enqueue and invoke attributeChangedCallback 
+PASS Calling Node.prototype.cloneNode(false) must push a new element queue to the processing stack 
 
index a3e987a..b58c5e4 100644 (file)
@@ -47,6 +47,42 @@ test(function () {
 
 }, 'setAttribute and removeAttribute must enqueue and invoke attributeChangedCallback');
 
+test(function () {
+    var shouldCloneAnotherInstance = false;
+    var anotherInstanceClone;
+    var log = [];
+
+    class SelfCloningElement extends HTMLElement {
+        constructor() {
+            super();
+            log.push([this, 'begin']);
+            if (shouldCloneAnotherInstance) {
+                shouldCloneAnotherInstance = false;
+                anotherInstanceClone = anotherInstance.cloneNode(false);
+            }
+            log.push([this, 'end']);
+        }
+    }
+    document.defineElement('self-cloning-element', SelfCloningElement);
+
+    var instance = document.createElement('self-cloning-element');
+    var anotherInstance = document.createElement('self-cloning-element');
+    shouldCloneAnotherInstance = true;
+
+    assert_equals(log.length, 4);
+    var instanceClone = instance.cloneNode(false);
+
+    assert_equals(log.length, 8);
+    assert_array_equals(log[0], [instance, 'begin']);
+    assert_array_equals(log[1], [instance, 'end']);
+    assert_array_equals(log[2], [anotherInstance, 'begin']);
+    assert_array_equals(log[3], [anotherInstance, 'end']);
+    assert_array_equals(log[4], [instanceClone, 'begin']);
+    assert_array_equals(log[5], [anotherInstanceClone, 'begin']);
+    assert_array_equals(log[6], [anotherInstanceClone, 'end']);
+    assert_array_equals(log[7], [instanceClone, 'end']);
+}, 'Calling Node.prototype.cloneNode(false) must push a new element queue to the processing stack');
+
 </script>
 </body>
 </html>
diff --git a/LayoutTests/fast/custom-elements/upgrading/Node-cloneNode-expected.txt b/LayoutTests/fast/custom-elements/upgrading/Node-cloneNode-expected.txt
new file mode 100644 (file)
index 0000000..05c1ef9
--- /dev/null
@@ -0,0 +1,9 @@
+
+PASS Node.prototype.cloneNode(false) must be able to clone a custom element 
+PASS Node.prototype.cloneNode(false) must be able to clone a custom element inside an iframe 
+PASS Node.prototype.cloneNode(true) must be able to clone a descendent custom element 
+PASS Node.prototype.cloneNode(true) must be able to clone a descendent custom element 
+PASS HTMLElement constructor must throw an InvalidStateError when the top of the construction stack is marked AlreadyConstructed due to a custom element constructor constructing itself after super() call 
+PASS HTMLElement constructor must throw an InvalidStateError when the top of the construction stack is marked AlreadyConstructed due to a custom element constructor constructing itself before super() call 
+PASS Upgrading a custom element must throw InvalidStateError when the custom element's constructor returns another element 
+
diff --git a/LayoutTests/fast/custom-elements/upgrading/Node-cloneNode.html b/LayoutTests/fast/custom-elements/upgrading/Node-cloneNode.html
new file mode 100644 (file)
index 0000000..9f4fcc3
--- /dev/null
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Extensions to Document interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Node.prototype.cloneNode should upgrade a custom element">
+<link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#upgrading">
+<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 withNewDocumentWithABrowsingContext(callback) {
+    var iframe = document.createElement('iframe');
+    document.body.appendChild(iframe);
+    try {
+        callback(iframe.contentWindow, iframe.contentDocument);        
+    } finally {
+        document.body.removeChild(iframe);
+    }
+}
+
+test(function () {
+    class MyCustomElement extends HTMLElement {}
+    document.defineElement('my-custom-element', MyCustomElement);
+
+    var instance = document.createElement('my-custom-element');
+    assert_true(instance instanceof HTMLElement);
+    assert_true(instance instanceof MyCustomElement);
+
+    var clone = instance.cloneNode(false);
+    assert_not_equals(instance, clone);
+    assert_true(clone instanceof HTMLElement,
+        'A cloned custom element must be an instance of HTMLElement');
+    assert_true(clone instanceof MyCustomElement,
+        'A cloned custom element must be an instance of the custom element');
+}, 'Node.prototype.cloneNode(false) must be able to clone a custom element');
+
+test(function () {
+    withNewDocumentWithABrowsingContext(function (contentWindow, contentDocument) {
+        class MyCustomElement extends contentWindow.HTMLElement {}
+        contentDocument.defineElement('my-custom-element', MyCustomElement);
+
+        var instance = contentDocument.createElement('my-custom-element');
+        assert_true(instance instanceof contentWindow.HTMLElement);
+        assert_true(instance instanceof MyCustomElement);
+
+        var clone = instance.cloneNode(false);
+        assert_not_equals(instance, clone);
+        assert_true(clone instanceof contentWindow.HTMLElement,
+            'A cloned custom element must be an instance of HTMLElement');
+        assert_true(clone instanceof MyCustomElement,
+            'A cloned custom element must be an instance of the custom element');
+    });
+}, 'Node.prototype.cloneNode(false) must be able to clone a custom element inside an iframe');
+
+test(function () {
+    withNewDocumentWithABrowsingContext(function (contentWindow, contentDocument) {
+        class MyCustomElement extends contentWindow.HTMLElement { }
+        contentDocument.defineElement('my-custom-element', MyCustomElement);
+
+        var instance = contentDocument.createElement('my-custom-element');
+        var container = contentDocument.createElement('div');
+        container.appendChild(instance);
+
+        var containerClone = container.cloneNode(true);
+        assert_true(containerClone instanceof contentWindow.HTMLDivElement);
+
+        var clone = containerClone.firstChild;
+        assert_not_equals(instance, clone);
+        assert_true(clone instanceof contentWindow.HTMLElement,
+            'A cloned custom element must be an instance of HTMLElement');
+        assert_true(clone instanceof MyCustomElement,
+            'A cloned custom element must be an instance of the custom element');
+    });
+}, 'Node.prototype.cloneNode(true) must be able to clone a descendent custom element');
+
+test(function () {
+    withNewDocumentWithABrowsingContext(function (contentWindow, contentDocument) {
+        var parentNodeInConstructor;
+        var previousSiblingInConstructor;
+        var nextSiblingInConstructor;
+        class MyCustomElement extends contentWindow.HTMLElement {
+            constructor() {
+                super();
+                parentNodeInConstructor = this.parentNode;
+                previousSiblingInConstructor = this.previousSibling;
+                nextSiblingInConstructor = this.nextSibling;
+            }
+        }
+        contentDocument.defineElement('my-custom-element', MyCustomElement);
+
+        var instance = contentDocument.createElement('my-custom-element');
+        var siblingBeforeInstance = contentDocument.createElement('b');
+        var siblingAfterInstance = contentDocument.createElement('a');
+        var container = contentDocument.createElement('div');
+        container.appendChild(siblingBeforeInstance);
+        container.appendChild(instance);
+        container.appendChild(siblingAfterInstance);
+
+        var containerClone = container.cloneNode(true);
+
+        assert_equals(parentNodeInConstructor, containerClone,
+            'An upgraded element must have its parentNode set before the custom element constructor is called');
+        assert_equals(previousSiblingInConstructor, containerClone.firstChild,
+            'An upgraded element must have its previousSibling set before the custom element constructor is called');
+        assert_equals(nextSiblingInConstructor, containerClone.lastChild,
+            'An upgraded element must have its nextSibling set before the custom element constructor is called');
+    });
+}, 'Node.prototype.cloneNode(true) must be able to clone a descendent custom element');
+
+test(function () {
+    withNewDocumentWithABrowsingContext(function (contentWindow, contentDocument) {
+        class MyCustomElement extends contentWindow.HTMLElement {
+            constructor(doNotCreateItself) {
+                super();
+                if (!doNotCreateItself)
+                    new MyCustomElement(true);
+            }
+        }
+        contentDocument.defineElement('my-custom-element', MyCustomElement);
+
+        var instance = contentDocument.createElement('my-custom-element');
+        assert_throws({'name': 'InvalidStateError'}, function () { instance.cloneNode(false); });
+    });
+}, 'HTMLElement constructor must throw an InvalidStateError when the top of the construction stack is marked AlreadyConstructed'
+    + ' due to a custom element constructor constructing itself after super() call');
+
+test(function () {
+    withNewDocumentWithABrowsingContext(function (contentWindow, contentDocument) {
+        class MyCustomElement extends contentWindow.HTMLElement {
+            constructor(doNotCreateItself) {
+                if (!doNotCreateItself)
+                    new MyCustomElement(true);
+                super();
+            }
+        }
+        contentDocument.defineElement('my-custom-element', MyCustomElement);
+
+        var instance = contentDocument.createElement('my-custom-element');
+        assert_throws({'name': 'InvalidStateError'}, function () { instance.cloneNode(false); });
+    });
+}, 'HTMLElement constructor must throw an InvalidStateError when the top of the construction stack is marked AlreadyConstructed'
+    + ' due to a custom element constructor constructing itself before super() call');
+
+test(function () {
+    withNewDocumentWithABrowsingContext(function (contentWindow, contentDocument) {
+        var cloning = false;
+        class MyCustomElement extends contentWindow.HTMLElement {
+            constructor() {
+                super();
+                if (cloning)
+                    return contentDocument.createElement('span');
+            }
+        }
+        contentDocument.defineElement('my-custom-element', MyCustomElement);
+
+        var instance = contentDocument.createElement('my-custom-element');
+        cloning = true;
+        assert_throws({'name': 'InvalidStateError'}, function () { instance.cloneNode(false); });
+    });
+}, 'Upgrading a custom element must throw InvalidStateError when the custom element\'s constructor returns another element');
+
+</script>
+</body>
+</html>
index dc5c074..cb9ab5e 100644 (file)
@@ -1,3 +1,52 @@
+2016-03-05  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Add the support for upgrading custom elements in cloneNode
+        https://bugs.webkit.org/show_bug.cgi?id=155062
+
+        Reviewed by Antti Koivisto.
+
+        Implement https://w3c.github.io/webcomponents/spec/custom/#upgrading and steps 6 through 11 in
+        https://w3c.github.io/webcomponents/spec/custom/#htmlelement-constructor to support upgrading elements
+        created by Node.prototype.cloneNode.
+
+        Tests: fast/custom-elements/lifecycle-callback-timing.html
+               fast/custom-elements/upgrading/Node-cloneNode.html
+
+        * bindings/js/JSCustomElementInterface.cpp:
+        (WebCore::JSCustomElementInterface::upgradeElement): Added. Implements
+         https://w3c.github.io/webcomponents/spec/custom/#dfn-upgrade-a-custom-element
+        (WebCore::JSCustomElementInterface::didUpgradeLastElementInConstructionStack): Added. Implements step 10
+         "Replace the last entry in definition's construction stacka with an already constructed marker."
+         in https://w3c.github.io/webcomponents/spec/custom/#dom-htmlelement-constructor
+        * bindings/js/JSCustomElementInterface.h:
+        (WebCore::JSCustomElementInterface::isUpgradingElement):
+        (WebCore::JSCustomElementInterface::lastElementInConstructionStack):
+        (WebCore::JSCustomElementInterface): Added m_constructionStack. This is the construction stack:
+         https://w3c.github.io/webcomponents/spec/custom/#dfn-element-definition-construction-stack
+        * bindings/js/JSDOMBinding.cpp:
+        (WebCore::throwInvalidStateError): Added.
+        * bindings/js/JSDOMBinding.h:
+        * bindings/js/JSHTMLElementCustom.cpp:
+        (WebCore::constructJSHTMLElement): Implement the upgrading case in:
+         https://w3c.github.io/webcomponents/spec/custom/#htmlelement-constructor
+        * dom/Document.cpp:
+        (WebCore::createFallbackHTMLElement): Added. Enqueues upgrades of custom elements (enqueueElementUpgrade
+         currently does nothing if there is no InvokesCustomElementLifecycleCallbacks; e.g. in other DOM APIs).
+         This function implements https://w3c.github.io/webcomponents/spec/custom/#dfn-element-upgrade-algorithm
+        (WebCore::Document::createElement):
+        * dom/LifecycleCallbackQueue.cpp:
+        (WebCore::LifecycleQueueItem::LifecycleQueueItem): Added a generic constructor.
+        (WebCore::LifecycleQueueItem::invoke): Call upgradeElement when m_type is Type::ElementUpgrade.
+        (WebCore::LifecycleCallbackQueue::enqueueElementUpgrade): Added.
+        * dom/LifecycleCallbackQueue.h:
+        * dom/Node.idl: Added InvokesCustomElementLifecycleCallbacks on cloneNode.
+        * dom/make_names.pl:
+        (printFactoryCppFile): Added a variant of createKnownElement which takes QualifiedName. Also directly call
+         find(HTML|SVG|MathML)ElementConstructorFunction in createElement that takes AtomicString to avoid an extra
+         function call.
+        (printFactoryHeaderFile): Added a function declaration for createKnownElement that takes QualifiedName and
+         outdented class and function declarations to match the modern code style guideline.
+
 2016-03-05  Tim Horton  <timothy_horton@apple.com>
 
         Create a DOMHTMLVideoElement when wrapping <video> elements
index 525974a..4823a81 100644 (file)
@@ -102,6 +102,54 @@ RefPtr<Element> JSCustomElementInterface::constructElement(const AtomicString& t
     return wrappedElement;
 }
 
+void JSCustomElementInterface::upgradeElement(Element& element)
+{
+    ASSERT(element.isCustomElement());
+    if (!canInvokeCallback())
+        return;
+
+    Ref<JSCustomElementInterface> protect(*this);
+    JSLockHolder lock(m_isolatedWorld->vm());
+
+    if (!m_constructor)
+        return;
+
+    ScriptExecutionContext* context = scriptExecutionContext();
+    if (!context)
+        return;
+    ASSERT(context->isDocument());
+    JSDOMGlobalObject* globalObject = toJSDOMGlobalObject(context, *m_isolatedWorld);
+    ExecState* state = globalObject->globalExec();
+    if (state->hadException())
+        return;
+
+    ConstructData constructData;
+    ConstructType constructType = m_constructor->methodTable()->getConstructData(m_constructor.get(), constructData);
+    if (constructType == ConstructType::None) {
+        ASSERT_NOT_REACHED();
+        return;
+    }
+
+    m_constructionStack.append(&element);
+
+    MarkedArgumentBuffer args;
+    InspectorInstrumentationCookie cookie = JSMainThreadExecState::instrumentFunctionConstruct(context, constructType, constructData);
+    JSValue returnedElement = construct(state, m_constructor.get(), constructType, constructData, args);
+    InspectorInstrumentation::didCallFunction(cookie, context);
+
+    m_constructionStack.removeLast();
+
+    if (state->hadException())
+        return;
+
+    Element* wrappedElement = JSElement::toWrapped(returnedElement);
+    if (!wrappedElement || wrappedElement != &element) {
+        throwInvalidStateError(*state, "Custom element constructor failed to upgrade an element");
+        return;
+    }
+    ASSERT(wrappedElement->isCustomElement());
+}
+
 void JSCustomElementInterface::attributeChanged(Element& element, const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
 {
     if (!canInvokeCallback())
@@ -145,6 +193,11 @@ void JSCustomElementInterface::attributeChanged(Element& element, const Qualifie
     if (exception)
         reportException(state, exception);
 }
+    
+void JSCustomElementInterface::didUpgradeLastElementInConstructionStack()
+{
+    m_constructionStack.last() = nullptr;
+}
 
 } // namespace WebCore
 
index 6c5544a..4177a6a 100644 (file)
@@ -63,6 +63,8 @@ public:
     enum class ShouldClearException { Clear, DoNotClear };
     RefPtr<Element> constructElement(const AtomicString&, ShouldClearException);
 
+    void upgradeElement(Element&);
+
     void attributeChanged(Element&, const QualifiedName&, const AtomicString& oldValue, const AtomicString& newValue);
 
     ScriptExecutionContext* scriptExecutionContext() const { return ContextDestructionObserver::scriptExecutionContext(); }
@@ -70,6 +72,10 @@ public:
 
     const QualifiedName& name() const { return m_name; }
 
+    bool isUpgradingElement() const { return !m_constructionStack.isEmpty(); }
+    Element* lastElementInConstructionStack() const { return m_constructionStack.last().get(); }
+    void didUpgradeLastElementInConstructionStack();
+
     virtual ~JSCustomElementInterface();
 
 private:
@@ -78,6 +84,7 @@ private:
     QualifiedName m_name;
     mutable JSC::Weak<JSC::JSObject> m_constructor;
     RefPtr<DOMWrapperWorld> m_isolatedWorld;
+    Vector<RefPtr<Element>, 1> m_constructionStack;
 };
 
 } // namespace WebCore
index 4033502..bfdcef4 100644 (file)
@@ -642,6 +642,13 @@ void throwNotSupportedError(JSC::ExecState& state, const char* message)
     state.vm().throwException(&state, createDOMException(&state, NOT_SUPPORTED_ERR, &messageString));
 }
 
+void throwInvalidStateError(JSC::ExecState& state, const char* message)
+{
+    ASSERT(!state.hadException());
+    String messageString(message);
+    state.vm().throwException(&state, createDOMException(&state, INVALID_STATE_ERR, &messageString));
+}
+
 JSC::EncodedJSValue throwArgumentMustBeEnumError(JSC::ExecState& state, unsigned argumentIndex, const char* argumentName, const char* functionInterfaceName, const char* functionName, const char* expectedValues)
 {
     StringBuilder builder;
index cccce32..0916de1 100644 (file)
@@ -85,6 +85,7 @@ WEBCORE_EXPORT JSC::EncodedJSValue reportDeprecatedGetterError(JSC::ExecState&,
 WEBCORE_EXPORT void reportDeprecatedSetterError(JSC::ExecState&, const char* interfaceName, const char* attributeName);
 
 void throwNotSupportedError(JSC::ExecState&, const char* message);
+void throwInvalidStateError(JSC::ExecState&, const char* message);
 void throwArrayElementTypeError(JSC::ExecState&);
 void throwAttributeTypeError(JSC::ExecState&, const char* interfaceName, const char* attributeName, const char* expectedType);
 WEBCORE_EXPORT void throwSequenceTypeError(JSC::ExecState&);
index 245e28f..bb790b4 100644 (file)
@@ -29,6 +29,7 @@
 #include "CustomElementDefinitions.h"
 #include "Document.h"
 #include "HTMLFormElement.h"
+#include "JSNodeCustom.h"
 #include <runtime/InternalFunction.h>
 #include <runtime/JSWithScope.h>
 
@@ -57,16 +58,40 @@ EncodedJSValue JSC_HOST_CALL constructJSHTMLElement(ExecState* state)
     if (!interface)
         return throwVMTypeError(state, "new.target does not define a custom element");
 
-    auto* globalObject = jsConstructor->globalObject();
-    Structure* baseStructure = getDOMStructure<JSHTMLElement>(vm, *globalObject);
-    auto* newElementStructure = InternalFunction::createSubclassStructure(state, newTargetValue, baseStructure);
-    if (UNLIKELY(state->hadException()))
+    if (!interface->isUpgradingElement()) {
+        auto* globalObject = jsConstructor->globalObject();
+        Structure* baseStructure = getDOMStructure<JSHTMLElement>(vm, *globalObject);
+        auto* newElementStructure = InternalFunction::createSubclassStructure(state, newTargetValue, baseStructure);
+        if (UNLIKELY(state->hadException()))
+            return JSValue::encode(jsUndefined());
+
+        Ref<HTMLElement> element = HTMLElement::create(interface->name(), document);
+        auto* jsElement = JSHTMLElement::create(newElementStructure, globalObject, element.get());
+        cacheWrapper(globalObject->world(), element.ptr(), jsElement);
+        return JSValue::encode(jsElement);
+    }
+
+    Element* elementToUpgrade = interface->lastElementInConstructionStack();
+    if (!elementToUpgrade) {
+        throwInvalidStateError(*state, "Cannot instantiate a custom element inside its own constrcutor during upgrades");
+        return JSValue::encode(jsUndefined());
+    }
+
+    JSValue elementWrapperValue = toJS(state, jsConstructor->globalObject(), elementToUpgrade);
+    ASSERT(elementWrapperValue.isObject());
+
+    JSValue newPrototype = newTarget->get(state, vm.propertyNames->prototype);
+    if (state->hadException())
+        return JSValue::encode(jsUndefined());
+
+    JSObject* elementWrapperObject = asObject(elementWrapperValue);
+    JSObject::setPrototype(elementWrapperObject, state, newPrototype, true /* shouldThrowIfCantSet */);
+    if (state->hadException())
         return JSValue::encode(jsUndefined());
 
-    Ref<HTMLElement> element = HTMLElement::create(interface->name(), document);
-    auto* jsElement = JSHTMLElement::create(newElementStructure, globalObject, element.get());
-    cacheWrapper(globalObject->world(), element.ptr(), jsElement);
-    return JSValue::encode(jsElement);
+    interface->didUpgradeLastElementInConstructionStack();
+
+    return JSValue::encode(elementWrapperValue);
 }
 #endif
 
index 0731fc5..675fefc 100644 (file)
 #include "JSModuleLoader.h"
 #include "KeyboardEvent.h"
 #include "Language.h"
+#include "LifecycleCallbackQueue.h"
 #include "LoaderStrategy.h"
 #include "Logging.h"
 #include "MainFrame.h"
@@ -1072,15 +1073,33 @@ bool Document::hasValidNamespaceForAttributes(const QualifiedName& qName)
     return hasValidNamespaceForElements(qName);
 }
 
+static Ref<HTMLElement> createFallbackHTMLElement(Document& document, const QualifiedName& name)
+{
+#if ENABLE(CUSTOM_ELEMENTS)
+    auto* definitions = document.customElementDefinitions();
+    if (UNLIKELY(definitions)) {
+        if (auto* interface = definitions->findInterface(name)) {
+            Ref<HTMLElement> element = HTMLElement::create(name, document);
+            element->setIsCustomElement(); // Pre-upgrade element is still considered a custom element.
+            LifecycleCallbackQueue::enqueueElementUpgrade(element.get(), *interface);
+            return element;
+        }
+    }
+#endif
+    return HTMLUnknownElement::create(name, document);
+}
+
 // FIXME: This should really be in a possible ElementFactory class.
 Ref<Element> Document::createElement(const QualifiedName& name, bool createdByParser)
 {
     RefPtr<Element> element;
 
     // FIXME: Use registered namespaces and look up in a hash to find the right factory.
-    if (name.namespaceURI() == xhtmlNamespaceURI)
-        element = HTMLElementFactory::createElement(name, *this, nullptr, createdByParser);
-    else if (name.namespaceURI() == SVGNames::svgNamespaceURI)
+    if (name.namespaceURI() == xhtmlNamespaceURI) {
+        element = HTMLElementFactory::createKnownElement(name, *this, nullptr, createdByParser);
+        if (UNLIKELY(!element))
+            element = createFallbackHTMLElement(*this, name);
+    } else if (name.namespaceURI() == SVGNames::svgNamespaceURI)
         element = SVGElementFactory::createElement(name, *this, createdByParser);
 #if ENABLE(MATHML)
     else if (name.namespaceURI() == MathMLNames::mathmlNamespaceURI)
index 385a9f6..fa3b2ab 100644 (file)
@@ -33,6 +33,7 @@
 #include "JSCustomElementInterface.h"
 #include "JSDOMBinding.h"
 #include <heap/Heap.h>
+#include <wtf/Optional.h>
 #include <wtf/Ref.h>
 
 namespace WebCore {
@@ -40,9 +41,16 @@ namespace WebCore {
 class LifecycleQueueItem {
 public:
     enum class Type {
+        ElementUpgrade,
         AttributeChanged,
     };
 
+    LifecycleQueueItem(Type type, Element& element, JSCustomElementInterface& interface)
+        : m_type(type)
+        , m_element(element)
+        , m_interface(interface)
+    { }
+
     LifecycleQueueItem(Element& element, JSCustomElementInterface& interface, const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
         : m_type(Type::AttributeChanged)
         , m_element(element)
@@ -54,14 +62,22 @@ public:
 
     void invoke()
     {
-        m_interface->attributeChanged(m_element.get(), m_attributeName, m_oldValue, m_newValue);
+        switch (m_type) {
+        case Type::ElementUpgrade:
+            m_interface->upgradeElement(m_element.get());
+            break;
+        case Type::AttributeChanged:
+            ASSERT(m_attributeName);
+            m_interface->attributeChanged(m_element.get(), m_attributeName.value(), m_oldValue, m_newValue);
+            break;
+        }
     }
 
 private:
     Type m_type;
     Ref<Element> m_element;
     Ref<JSCustomElementInterface> m_interface;
-    QualifiedName m_attributeName;
+    Optional<QualifiedName> m_attributeName;
     AtomicString m_oldValue;
     AtomicString m_newValue;
 };
@@ -74,6 +90,12 @@ LifecycleCallbackQueue::~LifecycleCallbackQueue()
     ASSERT(m_items.isEmpty());
 }
 
+void LifecycleCallbackQueue::enqueueElementUpgrade(Element& element, JSCustomElementInterface& interface)
+{
+    if (auto* queue = CustomElementLifecycleProcessingStack::ensureCurrentQueue())
+        queue->m_items.append(LifecycleQueueItem(LifecycleQueueItem::Type::ElementUpgrade, element, interface));
+}
+
 void LifecycleCallbackQueue::enqueueAttributeChangedCallback(Element& element, JSCustomElementInterface& interface,
     const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
 {
index de3b18e..03042a7 100644 (file)
@@ -46,6 +46,8 @@ public:
     LifecycleCallbackQueue();
     ~LifecycleCallbackQueue();
 
+    static void enqueueElementUpgrade(Element&, JSCustomElementInterface&);
+
     static void enqueueAttributeChangedCallback(Element&, JSCustomElementInterface&,
         const QualifiedName&, const AtomicString& oldValue, const AtomicString& newValue);
 
index 9250e54..2888be7 100644 (file)
     [Custom, RaisesException] Node appendChild([CustomReturn] Node newChild);
 
     boolean            hasChildNodes();
-    [NewObject, RaisesException, ImplementedAs=cloneNodeForBindings] Node cloneNode([Default=Undefined] optional boolean deep);
+
+    [NewObject, RaisesException, ImplementedAs=cloneNodeForBindings, InvokesCustomElementLifecycleCallbacks]
+    Node cloneNode([Default=Undefined] optional boolean deep);
+
     void               normalize();
 
     // Introduced in DOM Level 2:
index 1a070f7..8de96d4 100755 (executable)
@@ -1047,12 +1047,23 @@ RefPtr<$parameters{namespace}Element> $parameters{namespace}ElementFactory::crea
     return nullptr;
 }
 
-Ref<$parameters{namespace}Element> $parameters{namespace}ElementFactory::createElement(const AtomicString& name, Document& document$formElementArgumentForDefinition, bool createdByParser)
+RefPtr<$parameters{namespace}Element> $parameters{namespace}ElementFactory::createKnownElement(const QualifiedName& name, Document& document$formElementArgumentForDefinition, bool createdByParser)
 {
-    RefPtr<$parameters{namespace}Element> element = $parameters{namespace}ElementFactory::createKnownElement($argumentList);
-    if (LIKELY(element))
-        return element.releaseNonNull();
-    return $parameters{fallbackInterfaceName}::create(QualifiedName(nullAtom, name, ${lowercaseNamespacePrefix}NamespaceURI), document);
+    const ConstructorFunctionMapEntry& entry = find$parameters{namespace}ElementConstructorFunction(name.localName());
+    if (LIKELY(entry.function))
+        return entry.function($argumentList);
+    return nullptr;
+}
+
+Ref<$parameters{namespace}Element> $parameters{namespace}ElementFactory::createElement(const AtomicString& localName, Document& document$formElementArgumentForDefinition, bool createdByParser)
+{
+    const ConstructorFunctionMapEntry& entry = find$parameters{namespace}ElementConstructorFunction(localName);
+    if (LIKELY(entry.function)) {
+        ASSERT(entry.qualifiedName);
+        const auto& name = *entry.qualifiedName;
+        return entry.function($argumentList);
+    }
+    return $parameters{fallbackInterfaceName}::create(QualifiedName(nullAtom, localName, ${lowercaseNamespacePrefix}NamespaceURI), document);
 }
 
 Ref<$parameters{namespace}Element> $parameters{namespace}ElementFactory::createElement(const QualifiedName& name, Document& document$formElementArgumentForDefinition, bool createdByParser)
@@ -1089,29 +1100,36 @@ sub printFactoryHeaderFile
 
 namespace WebCore {
 
-    class Document;
-    class HTMLFormElement;
-    class QualifiedName;
+class Document;
+class HTMLFormElement;
+class QualifiedName;
 
-    class $parameters{namespace}Element;
+class $parameters{namespace}Element;
 
-    class $parameters{namespace}ElementFactory {
-    public:
+class $parameters{namespace}ElementFactory {
+public:
 END
 ;
 
-print F "        static RefPtr<$parameters{namespace}Element> createKnownElement(const AtomicString& localName, Document&";
+print F "static RefPtr<$parameters{namespace}Element> createKnownElement(const AtomicString&, Document&";
+print F ", HTMLFormElement* = nullptr" if $parameters{namespace} eq "HTML";
+print F ", bool createdByParser = false);\n";
+
+print F "static RefPtr<$parameters{namespace}Element> createKnownElement(const QualifiedName&, Document&";
 print F ", HTMLFormElement* = nullptr" if $parameters{namespace} eq "HTML";
-print F ", bool createdByParser = false);\n\n";
-print F "        static Ref<$parameters{namespace}Element> createElement(const AtomicString& localName, Document&";
+print F ", bool createdByParser = false);\n";
+
+print F "static Ref<$parameters{namespace}Element> createElement(const AtomicString&, Document&";
 print F ", HTMLFormElement* = nullptr" if $parameters{namespace} eq "HTML";
 print F ", bool createdByParser = false);\n";
-print F "        static Ref<$parameters{namespace}Element> createElement(const QualifiedName& localName, Document&";
+
+print F "static Ref<$parameters{namespace}Element> createElement(const QualifiedName&, Document&";
 print F ", HTMLFormElement* = nullptr" if $parameters{namespace} eq "HTML";
 print F ", bool createdByParser = false);\n";
 
 printf F<<END
-    };
+};
+
 }
 
 #endif // $parameters{namespace}ElementFactory_h