Implement form validation message UI
authortkent@chromium.org <tkent@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 14 Jan 2011 06:21:51 +0000 (06:21 +0000)
committertkent@chromium.org <tkent@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 14 Jan 2011 06:21:51 +0000 (06:21 +0000)
https://bugs.webkit.org/show_bug.cgi?id=48980

Reviewed by Dimitri Glazkov.

* Add four internal pseudo selectors:
  - -webkit-validation-bubble
  - -webkit-validation-bubble-message
  - -webkit-validation-bubble-top-outer-arrow
  - -webkit-validation-bubble-top-inner-arrow

* Implement ValidationMessage functions
  Show the message for <the number of characters> / 20.0 seconds.

No new tests because the feature is disabled by default for now and the
new behavior is strongly timing-dependent.

* css/CSSStyleSelector.cpp:
(WebCore::CSSStyleSelector::canShareStyleWithElement):
  Do not share a style with elements with different shadowPseudoId().
* css/html.css: Define appearance for the internal selectors.
(::-webkit-validation-bubble):
(::-webkit-validation-bubble-message):
(::-webkit-validation-bubble-top-outer-arrow):
(::-webkit-validation-bubble-top-inner-arrow):
* dom/Node.cpp:
(WebCore::Node::createRendererIfNeeded):
  Allow to add shadow renderers even if canHaveChildren() returns false.
* html/HTMLFormControlElement.cpp:
(WebCore::HTMLFormControlElement::detach):
  Remove m_validationMessage immediately because we can't use
  hideVisibleValidationMessage(), which calls a ValidationMessage function later.
(WebCore::HTMLFormControlElement::updateVisibleValidationMessage):
  - Don't create ValidationMessage if the message is empty.
  - Remove the check for message equality.
(WebCore::HTMLFormControlElement::hideVisibleValidationMessage):
  Don't remove m_validationMessage immediately. We shouldn't make the
  element needsLayout() state in this context.
* html/ValidationMessage.cpp:
(WebCore::ValidationMessage::~ValidationMessage):
  hideMessage() -> deleteBubbleTree() renaming.
(WebCore::ValidationMessage::setMessage): Implemented.
(WebCore::ValidationMessage::setMessageDOMAndStartTimer):
  Added. This updates the validation message and starts the timer to hide it.
(WebCore::ElementWithPseudoId):
  Added to help implementations of styled shadow nodes.
(WebCore::ValidationMessage::buildBubbleTree): Added.
(WebCore::ValidationMessage::requestToHideMessage): Added.
(WebCore::ValidationMessage::deleteBubbleTree):
  Renamed from hideMessage(), and implemented.
* html/ValidationMessage.h: Add declarations.

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

Source/WebCore/ChangeLog
Source/WebCore/css/CSSStyleSelector.cpp
Source/WebCore/css/html.css
Source/WebCore/dom/Node.cpp
Source/WebCore/html/HTMLFormControlElement.cpp
Source/WebCore/html/ValidationMessage.cpp
Source/WebCore/html/ValidationMessage.h

index 1967f9b..66e7741 100644 (file)
@@ -1,3 +1,57 @@
+2011-01-13  Kent Tamura  <tkent@chromium.org>
+
+        Reviewed by Dimitri Glazkov.
+
+        Implement form validation message UI
+        https://bugs.webkit.org/show_bug.cgi?id=48980
+
+        * Add four internal pseudo selectors:
+          - -webkit-validation-bubble
+          - -webkit-validation-bubble-message
+          - -webkit-validation-bubble-top-outer-arrow
+          - -webkit-validation-bubble-top-inner-arrow
+
+        * Implement ValidationMessage functions
+          Show the message for <the number of characters> / 20.0 seconds.
+
+        No new tests because the feature is disabled by default for now and the
+        new behavior is strongly timing-dependent.
+
+        * css/CSSStyleSelector.cpp:
+        (WebCore::CSSStyleSelector::canShareStyleWithElement):
+          Do not share a style with elements with different shadowPseudoId().
+        * css/html.css: Define appearance for the internal selectors.
+        (::-webkit-validation-bubble):
+        (::-webkit-validation-bubble-message):
+        (::-webkit-validation-bubble-top-outer-arrow):
+        (::-webkit-validation-bubble-top-inner-arrow):
+        * dom/Node.cpp:
+        (WebCore::Node::createRendererIfNeeded):
+          Allow to add shadow renderers even if canHaveChildren() returns false.
+        * html/HTMLFormControlElement.cpp:
+        (WebCore::HTMLFormControlElement::detach):
+          Remove m_validationMessage immediately because we can't use
+          hideVisibleValidationMessage(), which calls a ValidationMessage function later.
+        (WebCore::HTMLFormControlElement::updateVisibleValidationMessage):
+          - Don't create ValidationMessage if the message is empty.
+          - Remove the check for message equality.
+        (WebCore::HTMLFormControlElement::hideVisibleValidationMessage):
+          Don't remove m_validationMessage immediately. We shouldn't make the
+          element needsLayout() state in this context.
+        * html/ValidationMessage.cpp:
+        (WebCore::ValidationMessage::~ValidationMessage):
+          hideMessage() -> deleteBubbleTree() renaming.
+        (WebCore::ValidationMessage::setMessage): Implemented.
+        (WebCore::ValidationMessage::setMessageDOMAndStartTimer):
+          Added. This updates the validation message and starts the timer to hide it.
+        (WebCore::ElementWithPseudoId):
+          Added to help implementations of styled shadow nodes.
+        (WebCore::ValidationMessage::buildBubbleTree): Added.
+        (WebCore::ValidationMessage::requestToHideMessage): Added.
+        (WebCore::ValidationMessage::deleteBubbleTree):
+          Renamed from hideMessage(), and implemented.
+        * html/ValidationMessage.h: Add declarations.
+
 2011-01-13  Dan Bernstein  <mitz@apple.com>
 
         Reviewed by Alexey Proskuryakov.
index 6363ca9..b9f9bab 100644 (file)
@@ -981,6 +981,7 @@ bool CSSStyleSelector::canShareStyleWithElement(Node* n) const
             (s->hovered() == m_element->hovered()) &&
             (s->active() == m_element->active()) &&
             (s->focused() == m_element->focused()) &&
+            (s->shadowPseudoId() == m_element->shadowPseudoId()) &&
             (s != s->document()->cssTarget() && m_element != m_element->document()->cssTarget()) &&
             (s->fastGetAttribute(typeAttr) == m_element->fastGetAttribute(typeAttr)) &&
             (s->fastGetAttribute(XMLNames::langAttr) == m_element->fastGetAttribute(XMLNames::langAttr)) &&
index fb3a2c8..d5b3a19 100644 (file)
@@ -553,6 +553,59 @@ output {
     display: inline;
 }
 
+/* form validation message bubble */
+
+::-webkit-validation-bubble {
+    display: block;
+    z-index: 2147483647;
+    position: absolute;
+    opacity: 0.9;
+    line-height: 0;
+    -webkit-transition: opacity 05.5s ease;
+}
+
+::-webkit-validation-bubble-message {
+    display: block;
+    font: message-box;
+    min-width: 50px;
+    max-width: 200px;
+    border: solid 2px black;
+    background: -webkit-gradient(linear, left top, left bottom, from(#fbf9f9), to(#f0e4e4));
+    padding: 8px;
+    -webkit-border-radius: 8px;
+    -webkit-box-shadow: 4px 4px 4px rgba(204,204,204,0.7);
+    line-height: normal;
+}
+
+::-webkit-validation-bubble-top-outer-arrow {
+    display: inline-block;
+    position: relative;
+    left: 14px;
+    height: 0;
+    width: 0;
+    border-style: solid;
+    border-width: 14px;
+    border-bottom-color: black;
+    border-right-color: transparent;
+    border-top-width: 0;
+    border-left-width: 0;
+}
+
+::-webkit-validation-bubble-top-inner-arrow {
+    display: inline-block;
+    height: 0;
+    width: 0;
+    border-style: solid;
+    border-width: 10px; /* <border box width of outer-arrow> - <message border width> * 2 */
+    border-bottom-color: #fbf9f9;
+    border-right-color: transparent;
+    border-top-width: 0;
+    border-left-width: 0;
+    position: relative;
+    top: 2px; /* <message border width> */
+    left: 2px; /* <outer-arrow position> + <message border width> - <border box width of outer-arrow>  */
+}
+
 /* meter */
 
 meter {
index 7837707..08a9a47 100644 (file)
@@ -1380,8 +1380,10 @@ void Node::createRendererIfNeeded()
         document()->setFullScreenRenderer(fullscreenRenderer);
     }
 #endif
-    
-    if (parentRenderer && parentRenderer->canHaveChildren() && parent->childShouldCreateRenderer(this)) {
+
+    // FIXME: Ignoreing canHaveChildren() in a case of isShadowRoot() might be wrong.
+    // See https://bugs.webkit.org/show_bug.cgi?id=52423
+    if (parentRenderer && (parentRenderer->canHaveChildren() || isShadowRoot()) && parent->childShouldCreateRenderer(this)) {
         RefPtr<RenderStyle> style = styleForRenderer();
         if (rendererIsNeeded(style.get())) {
             if (RenderObject* r = createRenderer(document()->renderArena(), style.get())) {
index 714311e..b3ad7c8 100644 (file)
@@ -80,7 +80,7 @@ HTMLFormControlElement::~HTMLFormControlElement()
 
 void HTMLFormControlElement::detach()
 {
-    hideVisibleValidationMessage();
+    m_validationMessage = 0;
     HTMLElement::detach();
 }
 
@@ -310,18 +310,24 @@ void HTMLFormControlElement::updateVisibleValidationMessage()
             message.append(title);
         }
     }
+    if (message.isEmpty()) {
+        hideVisibleValidationMessage();
+        return;
+    }
     if (!m_validationMessage) {
         m_validationMessage = ValidationMessage::create(this);
         m_validationMessage->setMessage(message);
-    } else if (message.isEmpty())
-        hideVisibleValidationMessage();
-    else if (m_validationMessage->message() != message)
+    } else {
+        // Call setMessage() even if m_validationMesage->message() == message
+        // because the existing message might be to be hidden.
         m_validationMessage->setMessage(message);
+    }
 }
 
 void HTMLFormControlElement::hideVisibleValidationMessage()
 {
-    m_validationMessage = 0;
+    if (m_validationMessage)
+        m_validationMessage->requestToHideMessage();
 }
 
 String HTMLFormControlElement::visibleValidationMessage() const
index 4418235..2c441c7 100644 (file)
 #include "config.h"
 #include "ValidationMessage.h"
 
+#include "CSSStyleSelector.h"
+#include "FormAssociatedElement.h"
+#include "HTMLBRElement.h"
+#include "HTMLNames.h"
+#include "RenderObject.h"
+#include "Text.h"
 #include <wtf/PassOwnPtr.h>
 
 namespace WebCore {
 
+using namespace HTMLNames;
+
 ALWAYS_INLINE ValidationMessage::ValidationMessage(FormAssociatedElement* element)
     : m_element(element)
 {
@@ -42,7 +50,7 @@ ALWAYS_INLINE ValidationMessage::ValidationMessage(FormAssociatedElement* elemen
 
 ValidationMessage::~ValidationMessage()
 {
-    hideMessage();
+    deleteBubbleTree();
 }
 
 PassOwnPtr<ValidationMessage> ValidationMessage::create(FormAssociatedElement* element)
@@ -52,18 +60,105 @@ PassOwnPtr<ValidationMessage> ValidationMessage::create(FormAssociatedElement* e
 
 void ValidationMessage::setMessage(const String& message)
 {
-    // FIXME: Construct validation message UI if m_message is empty.
-
+    // Don't modify the DOM tree in this context.
+    // If so, an assertion in Node::isFocusable() fails.
+    ASSERT(!message.isEmpty());
     m_message = message;
+    if (!m_bubble)
+        m_timer.set(new Timer<ValidationMessage>(this, &ValidationMessage::buildBubbleTree));
+    else
+        m_timer.set(new Timer<ValidationMessage>(this, &ValidationMessage::setMessageDOMAndStartTimer));
+    m_timer->startOneShot(0);
+}
+
+void ValidationMessage::setMessageDOMAndStartTimer(Timer<ValidationMessage>*)
+{
+    ASSERT(m_bubbleMessage);
+    m_bubbleMessage->removeAllChildren();
+    Vector<String> lines;
+    m_message.split('\n', lines);
+    Document* doc = m_bubbleMessage->document();
+    ExceptionCode ec = 0;
+    for (unsigned i = 0; i < lines.size(); ++i) {
+        if (i) {
+            m_bubbleMessage->appendChild(HTMLBRElement::create(doc), ec);
+            m_bubbleMessage->appendChild(Text::create(doc, lines[i]), ec);
+        } else {
+            RefPtr<HTMLElement> bold = HTMLElement::create(bTag, doc);
+            bold->setInnerText(lines[i], ec);
+            m_bubbleMessage->appendChild(bold.release(), ec);
+        }
+    }
 
-    m_timer.set(new Timer<ValidationMessage>(this, &ValidationMessage::hideMessage));
-    m_timer->startOneShot(6.0); // FIXME: should be <message length> * something.
+    m_timer.set(new Timer<ValidationMessage>(this, &ValidationMessage::deleteBubbleTree));
+    m_timer->startOneShot(max(5.0, m_message.length() / 20.0));
 }
 
-void ValidationMessage::hideMessage(Timer<ValidationMessage>*)
+class ElementWithPseudoId : public HTMLElement {
+public:
+    static PassRefPtr<HTMLElement> create(Document* doc, const AtomicString& pseudoName)
+    {
+        return adoptRef(new ElementWithPseudoId(doc, pseudoName));
+    }
+
+protected:
+    ElementWithPseudoId(Document* doc, const AtomicString& pseudoName)
+        : HTMLElement(divTag, doc)
+        , m_pseudoName(pseudoName) { };
+    virtual AtomicString shadowPseudoId() const { return m_pseudoName; }
+
+private:
+    AtomicString m_pseudoName;
+};
+
+void ValidationMessage::buildBubbleTree(Timer<ValidationMessage>*)
 {
-    // FIXME: Implement.
+    HTMLElement* host = toHTMLElement(m_element);
+    Document* doc = host->document();
+    m_bubble = ElementWithPseudoId::create(doc, "-webkit-validation-bubble");
+    ExceptionCode ec = 0;
+    // FIXME: We need a way to host multiple shadow roots in a single node, or
+    // to inherit an existing shadow tree.
+    if (host->shadowRoot())
+        host->shadowRoot()->appendChild(m_bubble.get(), ec);
+    else {
+        host->setShadowRoot(m_bubble);
+        // FIXME: The following attach() should be unnecessary.
+        m_bubble->attach();
+    }
+
+    m_bubble->appendChild(ElementWithPseudoId::create(doc, "-webkit-validation-bubble-top-outer-arrow"), ec);
+    m_bubble->appendChild(ElementWithPseudoId::create(doc, "-webkit-validation-bubble-top-inner-arrow"), ec);
+    m_bubbleMessage = ElementWithPseudoId::create(doc, "-webkit-validation-bubble-message");
+    m_bubble->appendChild(m_bubbleMessage, ec);
 
+    setMessageDOMAndStartTimer();
+
+    // FIXME: Use transition to show the bubble.
+
+    // We don't need to adjust the bubble location. The default position is enough.
+}
+
+void ValidationMessage::requestToHideMessage()
+{
+    // We must not modify the DOM tree in this context by the same reason as setMessage().
+    m_timer.set(new Timer<ValidationMessage>(this, &ValidationMessage::deleteBubbleTree));
+    m_timer->startOneShot(0);
+}
+
+void ValidationMessage::deleteBubbleTree(Timer<ValidationMessage>*)
+{
+    if (m_bubble) {
+        m_bubbleMessage = 0;
+        HTMLElement* host = toHTMLElement(m_element);
+        if (m_bubble->isShadowRoot())
+            host->setShadowRoot(0);
+        else {
+            ExceptionCode ec;
+            host->shadowRoot()->removeChild(m_bubble.get(), ec);
+        }
+        m_bubble = 0;
+    }
     m_message = String();
 }
 
index d78e3f3..5fa1f96 100644 (file)
 #include "Timer.h"
 #include <wtf/Noncopyable.h>
 #include <wtf/OwnPtr.h>
+#include <wtf/RefPtr.h>
 #include <wtf/text/WTFString.h>
 
 namespace WebCore {
 
 class FormAssociatedElement;
+class HTMLElement;
 
 class ValidationMessage : public Noncopyable {
 public:
@@ -46,14 +48,19 @@ public:
     ~ValidationMessage();
     String message() const { return m_message; }
     void setMessage(const String&);
+    void requestToHideMessage();
 
 private:
     ValidationMessage(FormAssociatedElement*);
-    void hideMessage(Timer<ValidationMessage>* = 0);
+    void setMessageDOMAndStartTimer(Timer<ValidationMessage>* = 0);
+    void buildBubbleTree(Timer<ValidationMessage>*);
+    void deleteBubbleTree(Timer<ValidationMessage>* = 0);
 
     FormAssociatedElement* m_element;
     String m_message;
     OwnPtr<Timer<ValidationMessage> > m_timer;
+    RefPtr<HTMLElement> m_bubble;
+    RefPtr<HTMLElement> m_bubbleMessage;
 };
 
 } // namespace WebCore