Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "TextControlElement.h"
#include "mozilla/IMEContentObserver.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/TextControlState.h"
#include "mozilla/TextEditor.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/ShadowRoot.h"
#include "nsIFormControl.h"
#include "nsTextNode.h"
using namespace mozilla::dom;
namespace mozilla {
static RefPtr<Element> MakeAnonElement(Document& aDoc,
PseudoStyleType aPseudoType,
nsAtom* aTag = nsGkAtoms::div) {
MOZ_ASSERT(aPseudoType != PseudoStyleType::NotPseudo);
RefPtr<Element> element = aDoc.CreateHTMLElement(aTag);
element->SetPseudoElementType(aPseudoType);
if (aPseudoType == PseudoStyleType::MozTextControlEditingRoot) {
// Make our root node editable
element->SetFlags(NODE_IS_EDITABLE);
} else {
// The text control's accessible takes care of the placeholder etc for us,
// all our pseudo-elements other than the root should not show up in the
// a11y tree.
element->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, u"true"_ns,
false);
}
return element;
}
RefPtr<Element> MakePlaceholderOrPreview(Document& aDoc,
PseudoStyleType aPseudoType,
const nsAString& aValue) {
RefPtr el = MakeAnonElement(aDoc, aPseudoType);
RefPtr text = aDoc.CreateTextNode(aValue);
el->AppendChildTo(text, false, IgnoreErrors());
return el;
}
Element* TextControlElement::FindShadowPseudo(PseudoStyleType aType) const {
auto* sr = GetShadowRoot();
if (!sr) {
return nullptr;
}
for (auto* child = sr->GetFirstChild(); child;
child = child->GetNextSibling()) {
auto* el = Element::FromNode(child);
if (el->GetPseudoElementType() == aType) {
return el;
}
}
return nullptr;
}
void TextControlElement::GetPreviewValue(nsAString& aValue) {
Element* existing = FindShadowPseudo(PseudoStyleType::MozTextControlPreview);
if (!existing) {
return;
}
auto* text = Text::FromNodeOrNull(existing->GetFirstChild());
if (NS_WARN_IF(!text)) {
return;
}
text->GetData(aValue);
}
void TextControlElement::SetPreviewValue(const nsAString& aValue) {
RefPtr sr = GetShadowRoot();
if (!sr) {
return;
}
RefPtr existing = FindShadowPseudo(PseudoStyleType::MozTextControlPreview);
if (aValue.IsEmpty()) {
if (existing) {
existing->Remove();
}
return;
}
if (existing) {
RefPtr text = Text::FromNodeOrNull(existing->GetFirstChild());
if (NS_WARN_IF(!text)) {
return;
}
text->SetData(aValue, IgnoreErrors());
return;
}
// Preview goes before the root (and after placeholder if present).
RefPtr editingRoot =
FindShadowPseudo(PseudoStyleType::MozTextControlEditingRoot);
if (NS_WARN_IF(!editingRoot)) {
// This can happen if we get called on e.g. a datetimebox or so.
return;
}
RefPtr preview = MakePlaceholderOrPreview(
*OwnerDoc(), PseudoStyleType::MozTextControlPreview, aValue);
sr->InsertChildBefore(preview, editingRoot, /* aNotify = */ true,
IgnoreErrors());
}
static void ProcessPlaceholder(nsAString& aValue, bool aTextArea) {
if (aTextArea) { // <textarea>s preserve newlines...
nsContentUtils::PlatformToDOMLineBreaks(aValue);
} else { // ...<input>s don't
nsContentUtils::RemoveNewlines(aValue);
}
}
void TextControlElement::UpdatePlaceholder(const nsAttrValue* aOldValue,
const nsAttrValue* aNewValue) {
RefPtr sr = GetShadowRoot();
if (!sr) {
return;
}
if (!IsSingleLineTextControlOrTextArea()) {
// We may still have a shadow tree for other input types like
// <input type=date>
return;
}
if (aOldValue) {
RefPtr existing = FindShadowPseudo(PseudoStyleType::Placeholder);
if (NS_WARN_IF(!existing)) {
return;
}
if (!aNewValue) {
existing->Remove();
return;
}
RefPtr text = Text::FromNodeOrNull(existing->GetFirstChild());
if (NS_WARN_IF(!text)) {
return;
}
nsAutoString value;
aNewValue->ToString(value);
ProcessPlaceholder(value, IsTextArea());
text->SetData(value, IgnoreErrors());
return;
}
MOZ_ASSERT(aNewValue, "No need to call this if the attribute didn't change");
MOZ_ASSERT(!FindShadowPseudo(PseudoStyleType::Placeholder));
nsAutoString value;
aNewValue->ToString(value);
ProcessPlaceholder(value, IsTextArea());
RefPtr ph = MakePlaceholderOrPreview(*OwnerDoc(),
PseudoStyleType::Placeholder, value);
// ::placeholder is always the first child, see SetupShadowTree().
sr->InsertChildBefore(ph, sr->GetFirstChild(), /* aNotify = */ true,
IgnoreErrors());
}
void TextControlElement::SetupShadowTree(ShadowRoot& aShadow, bool aNotify) {
MOZ_ASSERT(IsSingleLineTextControlOrTextArea());
auto& doc = *OwnerDoc();
nsAutoString value;
if (GetAttr(nsGkAtoms::placeholder, value)) {
ProcessPlaceholder(value, IsTextArea());
RefPtr ph =
MakePlaceholderOrPreview(doc, PseudoStyleType::Placeholder, value);
aShadow.AppendChildTo(ph, aNotify, IgnoreErrors());
}
const bool isPassword = mType == FormControlType::InputPassword;
RefPtr root =
MakeAnonElement(doc, PseudoStyleType::MozTextControlEditingRoot);
{
RefPtr text = doc.CreateEmptyTextNode();
text->MarkAsMaybeModifiedFrequently();
if (isPassword) {
text->MarkAsMaybeMasked();
}
root->AppendChildTo(text, false, IgnoreErrors());
}
aShadow.AppendChildTo(root, aNotify, IgnoreErrors());
auto button = [&]() -> RefPtr<Element> {
switch (mType) {
case FormControlType::InputPassword:
if (StaticPrefs::layout_forms_reveal_password_button_enabled() ||
doc.ChromeRulesEnabled()) {
RefPtr button = MakeAnonElement(doc, PseudoStyleType::MozReveal,
nsGkAtoms::button);
button->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns,
false);
return button;
}
break;
case FormControlType::InputSearch: {
// Bug 1936648: Until we're absolutely sure we've solved the
// accessibility issues around the clear search button, we're only
// enabling the clear button in chrome contexts. See also Bug 1655503
if (StaticPrefs::layout_forms_input_type_search_enabled() ||
doc.ChromeRulesEnabled()) {
// Create the ::-moz-search-clear-button pseudo-element:
RefPtr button = MakeAnonElement(
doc, PseudoStyleType::MozSearchClearButton, nsGkAtoms::button);
button->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns,
false);
button->SetAttr(kNameSpaceID_None, nsGkAtoms::title, u""_ns, false);
return button;
}
break;
}
#ifndef ANDROID
case FormControlType::InputNumber: {
// Create the ::-moz-number-spin-box pseudo-element:
RefPtr button = MakeAnonElement(doc, PseudoStyleType::MozNumberSpinBox);
// Create the ::-moz-number-spin-up/down pseudo-elements:
for (auto pseudo : {PseudoStyleType::MozNumberSpinUp,
PseudoStyleType::MozNumberSpinDown}) {
RefPtr spinner = MakeAnonElement(doc, pseudo);
button->AppendChildTo(spinner, false, IgnoreErrors());
}
return button;
}
#endif
default:
break;
}
return nullptr;
}();
if (button) {
MOZ_ASSERT(IsButtonPseudoElement(button->GetPseudoElementType()));
aShadow.AppendChildTo(button, aNotify, IgnoreErrors());
}
UpdateValueDisplay(aNotify);
}
bool TextControlElement::IsButtonPseudoElement(PseudoStyleType aType) {
switch (aType) {
case PseudoStyleType::MozSearchClearButton:
case PseudoStyleType::MozNumberSpinBox:
case PseudoStyleType::MozReveal:
return true;
default:
break;
}
return false;
}
Element* TextControlElement::GetTextEditorRoot() const {
return FindShadowPseudo(PseudoStyleType::MozTextControlEditingRoot);
}
Element* TextControlElement::GetTextEditorPlaceholder() const {
return FindShadowPseudo(PseudoStyleType::Placeholder);
}
Element* TextControlElement::GetTextEditorPreview() const {
return FindShadowPseudo(PseudoStyleType::MozTextControlPreview);
}
Element* TextControlElement::GetTextEditorButton() const {
auto* sr = GetShadowRoot();
if (!sr) {
return nullptr;
}
auto* el = Element::FromNodeOrNull(sr->GetLastChild());
if (!el || !IsButtonPseudoElement(el->GetPseudoElementType())) {
return nullptr;
}
return el;
}
void TextControlElement::UpdateValueDisplay(bool aNotify) {
auto* root = GetTextEditorRoot();
if (!root) {
return;
}
auto* textContent = Text::FromNodeOrNull(root->GetFirstChild());
if (NS_WARN_IF(!textContent)) {
return;
}
// Get the current value of the textfield from the content.
nsAutoString value;
GetTextEditorValue(value);
textContent->SetText(value, aNotify);
}
} // namespace mozilla