blob: 61349c87b783be70e76ca0e123259ae512bdab02 [file] [log] [blame]
/*
* Copyright (C) 2010, 2012 Google Inc. All rights reserved.
* Copyright (C) 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "ValidationMessage.h"
#include "CSSPropertyNames.h"
#include "CSSValueKeywords.h"
#include "ElementInlines.h"
#include "HTMLBRElement.h"
#include "HTMLDivElement.h"
#include "HTMLFormControlElement.h"
#include "HTMLNames.h"
#include "Page.h"
#include "RenderBlock.h"
#include "RenderObject.h"
#include "ScriptDisallowedScope.h"
#include "Settings.h"
#include "ShadowPseudoIds.h"
#include "ShadowRoot.h"
#include "StyleResolver.h"
#include "Text.h"
#include "ValidationMessageClient.h"
namespace WebCore {
using namespace HTMLNames;
ValidationMessage::ValidationMessage(HTMLFormControlElement* element)
: m_element(element)
{
ASSERT(m_element);
}
ValidationMessage::~ValidationMessage()
{
if (ValidationMessageClient* client = validationMessageClient()) {
client->hideValidationMessage(*m_element);
return;
}
deleteBubbleTree();
}
ValidationMessageClient* ValidationMessage::validationMessageClient() const
{
if (Page* page = m_element->document().page())
return page->validationMessageClient();
return 0;
}
void ValidationMessage::updateValidationMessage(const String& message)
{
// We want to hide the validation message as soon as the user starts
// typing, even if a constraint is still violated. Thefore, we hide the message instead
// of updating it if it is already visible.
if (isVisible()) {
requestToHideMessage();
return;
}
String updatedMessage = message;
if (!validationMessageClient()) {
// HTML5 specification doesn't ask UA to show the title attribute value
// with the validationMessage. However, this behavior is same as Opera
// and the specification describes such behavior as an example.
if (!updatedMessage.isEmpty()) {
const AtomString& title = m_element->attributeWithoutSynchronization(titleAttr);
if (!title.isEmpty())
updatedMessage = updatedMessage + '\n' + title;
}
}
if (updatedMessage.isEmpty()) {
requestToHideMessage();
return;
}
setMessage(updatedMessage);
}
void ValidationMessage::setMessage(const String& message)
{
if (ValidationMessageClient* client = validationMessageClient()) {
client->showValidationMessage(*m_element, message);
return;
}
// Don't modify the DOM tree in this context.
// If so, an assertion in Element::isFocusable() fails.
ASSERT(!message.isEmpty());
m_message = message;
if (!m_bubble)
m_timer = makeUnique<Timer>(*this, &ValidationMessage::buildBubbleTree);
else
m_timer = makeUnique<Timer>(*this, &ValidationMessage::setMessageDOMAndStartTimer);
m_timer->startOneShot(0_s);
}
void ValidationMessage::setMessageDOMAndStartTimer()
{
ASSERT(!validationMessageClient());
ASSERT(m_messageHeading);
ASSERT(m_messageBody);
m_messageHeading->removeChildren();
m_messageBody->removeChildren();
auto& document = m_messageHeading->document();
auto lines = StringView(m_message).split('\n');
auto it = lines.begin();
if (it != lines.end()) {
m_messageHeading->setInnerText((*it).toString());
auto firstBodyLineIterator = ++it;
for (; it != lines.end(); ++it) {
if (it != firstBodyLineIterator)
m_messageBody->appendChild(HTMLBRElement::create(document));
m_messageBody->appendChild(Text::create(document, (*it).toString()));
}
}
int magnification = document.page() ? document.page()->settings().validationMessageTimerMagnification() : -1;
if (magnification <= 0)
m_timer = nullptr;
else {
m_timer = makeUnique<Timer>(*this, &ValidationMessage::deleteBubbleTree);
m_timer->startOneShot(std::max(5_s, 1_ms * static_cast<double>(m_message.length()) * magnification));
}
}
void ValidationMessage::adjustBubblePosition()
{
if (!m_bubble)
return;
if (!m_element->renderer())
return;
LayoutRect hostRect = m_element->renderer()->absoluteBoundingBoxRect();
if (hostRect.isEmpty())
return;
double hostX = hostRect.x();
double hostY = hostRect.y();
if (RenderObject* renderer = m_bubble->renderer()) {
if (RenderBox* container = renderer->containingBlock()) {
FloatPoint containerLocation = container->localToAbsolute();
hostX -= containerLocation.x() + container->borderLeft();
hostY -= containerLocation.y() + container->borderTop();
}
}
m_bubble->setInlineStyleProperty(CSSPropertyTop, hostY + hostRect.height(), CSSUnitType::CSS_PX);
// The 'left' value of ::-webkit-validation-bubble-arrow.
const int bubbleArrowTopOffset = 32;
double bubbleX = hostX;
if (hostRect.width() / 2 < bubbleArrowTopOffset)
bubbleX = std::max(hostX + hostRect.width() / 2 - bubbleArrowTopOffset, 0.0);
m_bubble->setInlineStyleProperty(CSSPropertyLeft, bubbleX, CSSUnitType::CSS_PX);
}
void ValidationMessage::buildBubbleTree()
{
ASSERT(!validationMessageClient());
if (!m_element->renderer())
return;
Ref shadowRoot = m_element->ensureUserAgentShadowRoot();
ScriptDisallowedScope::InMainThread scriptDisallowedScope;
ScriptDisallowedScope::EventAllowedScope allowedScope(shadowRoot);
Document& document = m_element->document();
m_bubble = HTMLDivElement::create(document);
shadowRoot->appendChild(*m_bubble);
m_bubble->setPseudo(ShadowPseudoIds::webkitValidationBubble());
// Need to force position:absolute because RenderMenuList doesn't assume it
// contains non-absolute or non-fixed renderers as children.
m_bubble->setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
auto clipper = HTMLDivElement::create(document);
m_bubble->appendChild(clipper);
clipper->setPseudo(ShadowPseudoIds::webkitValidationBubbleArrowClipper());
auto bubbleArrow = HTMLDivElement::create(document);
clipper->appendChild(bubbleArrow);
bubbleArrow->setPseudo(ShadowPseudoIds::webkitValidationBubbleArrow());
auto message = HTMLDivElement::create(document);
m_bubble->appendChild(message);
message->setPseudo(ShadowPseudoIds::webkitValidationBubbleMessage());
auto icon = HTMLDivElement::create(document);
message->appendChild(icon);
icon->setPseudo(ShadowPseudoIds::webkitValidationBubbleIcon());
auto textBlock = HTMLDivElement::create(document);
message->appendChild(textBlock);
textBlock->setPseudo(ShadowPseudoIds::webkitValidationBubbleTextBlock());
m_messageHeading = HTMLDivElement::create(document);
textBlock->appendChild(*m_messageHeading);
m_messageHeading->setPseudo(ShadowPseudoIds::webkitValidationBubbleHeading());
m_messageBody = HTMLDivElement::create(document);
textBlock->appendChild(*m_messageBody);
m_messageBody->setPseudo(ShadowPseudoIds::webkitValidationBubbleBody());
setMessageDOMAndStartTimer();
// FIXME: Use transition to show the bubble.
if (!document.view())
return;
document.view()->queuePostLayoutCallback([weakThis = WeakPtr { *this }] {
if (!weakThis)
return;
weakThis->adjustBubblePosition();
});
}
void ValidationMessage::requestToHideMessage()
{
if (ValidationMessageClient* client = validationMessageClient()) {
client->hideValidationMessage(*m_element);
return;
}
// We must not modify the DOM tree in this context by the same reason as setMessage().
m_timer = makeUnique<Timer>(*this, &ValidationMessage::deleteBubbleTree);
m_timer->startOneShot(0_s);
}
bool ValidationMessage::shadowTreeContains(const Node& node) const
{
if (validationMessageClient() || !m_bubble)
return false;
return &m_bubble->treeScope() == &node.treeScope();
}
void ValidationMessage::deleteBubbleTree()
{
ASSERT(!validationMessageClient());
if (m_bubble) {
ScriptDisallowedScope::EventAllowedScope allowedScope(*m_element->userAgentShadowRoot());
m_messageHeading = nullptr;
m_messageBody = nullptr;
m_element->userAgentShadowRoot()->removeChild(*m_bubble);
m_bubble = nullptr;
}
m_message = String();
}
bool ValidationMessage::isVisible() const
{
if (ValidationMessageClient* client = validationMessageClient())
return client->isValidationMessageVisible(*m_element);
return !m_message.isEmpty();
}
} // namespace WebCore