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/ContentEvents.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/IMEContentObserver.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/PresShell.h"
#include "mozilla/TextControlState.h"
#include "mozilla/TextEditor.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/ShadowRoot.h"
#include "nsFocusManager.h"
#include "nsFrameSelection.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());
}
static RefPtr<Element> CreateButton(FormControlType aType, Document& aDoc) {
switch (aType) {
case FormControlType::InputPassword:
if (StaticPrefs::layout_forms_reveal_password_button_enabled() ||
aDoc.ChromeRulesEnabled()) {
RefPtr button = MakeAnonElement(aDoc, 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() ||
aDoc.ChromeRulesEnabled()) {
// Create the ::-moz-search-clear-button pseudo-element:
RefPtr button = MakeAnonElement(
aDoc, 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(aDoc, PseudoStyleType::MozNumberSpinBox);
// Create the ::-moz-number-spin-up/down pseudo-elements:
for (auto pseudo : {PseudoStyleType::MozNumberSpinUp,
PseudoStyleType::MozNumberSpinDown}) {
RefPtr spinner = MakeAnonElement(aDoc, pseudo);
button->AppendChildTo(spinner, false, IgnoreErrors());
}
return button;
}
#endif
default:
break;
}
return nullptr;
}
void TextControlElement::UpdateTextEditorShadowTree() {
Element* root = GetTextEditorRoot();
if (!root) {
// We might not have created the shadow tree yet.
return;
}
auto* text = Text::FromNodeOrNull(root->GetFirstChild());
if (!text) {
MOZ_DIAGNOSTIC_ASSERT(false, "There should be editable text");
return;
}
if (IsPasswordTextControl()) {
text->MarkAsMaybeMasked();
} else {
text->UnsetFlags(NS_MAYBE_MASKED);
}
if (RefPtr<Element> existing = GetTextEditorButton()) {
existing->Remove();
}
auto& doc = *OwnerDoc();
if (RefPtr<Element> button = CreateButton(mType, doc)) {
MOZ_ASSERT(IsButtonPseudoElement(button->GetPseudoElementType()));
ShadowRoot* shadowRoot = GetShadowRoot();
shadowRoot->AppendChildTo(button, 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());
if (RefPtr<Element> button = CreateButton(mType, doc)) {
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);
}
static bool SelectTextFieldOnFocus() {
return LookAndFeel::GetInt(LookAndFeel::IntID::SelectTextfieldsOnKeyFocus);
}
void TextControlElement::ScrollSelectionIntoViewAsync(
ScrollAncestors aScrollAncestors) {
nsCOMPtr<nsISelectionController> selCon = GetSelectionController();
if (!selCon) {
return;
}
// Scroll the selection into view (see bug 231389).
const auto flags = aScrollAncestors == ScrollAncestors::Yes
? ScrollFlags::None
: ScrollFlags::ScrollFirstAncestorOnly;
selCon->ScrollSelectionIntoView(
SelectionType::eNormal, nsISelectionController::SELECTION_FOCUS_REGION,
ScrollAxis(), ScrollAxis(), flags);
}
void TextControlElement::ShowSelection() {
nsISelectionController* selCon = GetSelectionController();
if (!selCon) {
return;
}
RefPtr<Selection> ourSel =
selCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
if (!ourSel) {
return;
}
auto* ps = OwnerDoc()->GetPresShell();
if (!ps) {
return;
}
RefPtr<nsCaret> caret = ps->GetCaret();
if (!caret) {
return;
}
// Tell the caret to use our selection
caret->SetSelection(ourSel);
// mutual-exclusion: the selection is either controlled by the document or by
// the text input/area. Clear any selection in the document since the focus is
// now on our independent selection.
RefPtr<Selection> docSel =
ps->GetSelection(nsISelectionController::SELECTION_NORMAL);
if (!docSel) {
return;
}
if (!docSel->IsCollapsed()) {
docSel->RemoveAllRanges(IgnoreErrors());
}
// If the focus moved to a text control during text selection by pointer
// device, stop extending the selection.
if (RefPtr<nsFrameSelection> frameSelection = ps->FrameSelection()) {
frameSelection->SetDragState(false);
}
}
bool TextControlElement::NeedToInitializeEditorForEvent(
EventChainPreVisitor& aVisitor) const {
switch (aVisitor.mEvent->mMessage) {
case eVoidEvent:
case eMouseMove:
case eMouseEnterIntoWidget:
case eMouseExitFromWidget:
case eMouseOver:
case eMouseOut:
case eScrollPortUnderflow:
case eScrollPortOverflow:
return false;
default:
return true;
}
}
void TextControlElement::OnFocus(const WidgetEvent& aFocusEvent) {
MOZ_ASSERT(aFocusEvent.mMessage == eFocus);
if (!IsInComposedDoc()) {
return;
}
ShowSelection();
// See if we should select the contents of the textbox. This happens
// for text and password fields when the field was focused by the
// keyboard or a navigation, the platform allows it, and it wasn't
// just because we raised a window.
//
// While it'd usually make sense, we don't do this for JS callers
// because it causes some compat issues, see bug 1712724 for example.
nsFocusManager* fm = nsFocusManager::GetFocusManager();
if (!IsTextArea() && !aFocusEvent.AsFocusEvent()->mFromRaise &&
SelectTextFieldOnFocus()) {
uint32_t lastFocusMethod = fm->GetLastFocusMethod(OwnerDoc()->GetWindow());
const bool shouldSelectAllOnFocus = [&] {
if (lastFocusMethod & nsIFocusManager::FLAG_BYMOVEFOCUS) {
return true;
}
if (lastFocusMethod & nsIFocusManager::FLAG_BYJS) {
return false;
}
return bool(lastFocusMethod & nsIFocusManager::FLAG_BYKEY);
}();
if (shouldSelectAllOnFocus) {
SelectAll();
}
}
}
void TextControlElement::SelectAll() {
if (auto* state = GetTextControlState()) {
state->SetSelectionRange(0, UINT32_MAX, Optional<nsAString>(),
IgnoreErrors(),
TextControlState::ScrollAfterSelection::No);
}
}
} // namespace mozilla