This implements the styles & machinery for ::picker to be shown as a anchor-pos popover, instead of the SelectParent - when in appearance base. Differential Revision: https://phabricator.services.mozilla.com/D305622
445 lines
14 KiB
C++
445 lines
14 KiB
C++
/* 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/HTMLBRElement.h"
|
|
#include "mozilla/dom/ShadowRoot.h"
|
|
#include "nsFocusManager.h"
|
|
#include "nsFrameSelection.h"
|
|
#include "nsIFormControl.h"
|
|
#include "nsTextControlFrame.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;
|
|
}
|
|
|
|
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 after the root or placeholder.
|
|
RefPtr prevSibling = FindShadowPseudo(PseudoStyleType::Placeholder);
|
|
if (!prevSibling) {
|
|
prevSibling = FindShadowPseudo(PseudoStyleType::MozTextControlEditingRoot);
|
|
}
|
|
if (NS_WARN_IF(!prevSibling)) {
|
|
// 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, prevSibling->GetNextSibling(),
|
|
/* 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 goes after editing root.
|
|
RefPtr editingRoot =
|
|
FindShadowPseudo(PseudoStyleType::MozTextControlEditingRoot);
|
|
if (NS_WARN_IF(!editingRoot)) {
|
|
return;
|
|
}
|
|
sr->InsertChildBefore(ph, editingRoot->GetNextSibling(), /* aNotify = */ true,
|
|
IgnoreErrors());
|
|
}
|
|
|
|
already_AddRefed<Element> TextControlElement::CreateButton() const {
|
|
auto& doc = *OwnerDoc();
|
|
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.forget();
|
|
}
|
|
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()) {
|
|
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.forget();
|
|
}
|
|
break;
|
|
}
|
|
#ifndef ANDROID
|
|
case FormControlType::InputNumber: {
|
|
RefPtr button = MakeAnonElement(doc, PseudoStyleType::MozNumberSpinBox);
|
|
for (auto pseudo : {PseudoStyleType::MozNumberSpinUp,
|
|
PseudoStyleType::MozNumberSpinDown}) {
|
|
RefPtr spinner = MakeAnonElement(doc, pseudo);
|
|
button->AppendChildTo(spinner, false, IgnoreErrors());
|
|
}
|
|
return button.forget();
|
|
}
|
|
#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);
|
|
}
|
|
}
|
|
|
|
void TextControlElement::SetupShadowTree(ShadowRoot& aShadow, bool aNotify) {
|
|
MOZ_ASSERT(IsSingleLineTextControlOrTextArea());
|
|
auto& doc = *OwnerDoc();
|
|
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());
|
|
RefPtr br = doc.CreateHTMLElement(nsGkAtoms::br);
|
|
br->SetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE);
|
|
root->AppendChildTo(br, false, IgnoreErrors());
|
|
}
|
|
aShadow.AppendChildTo(root, aNotify, IgnoreErrors());
|
|
|
|
nsAutoString value;
|
|
if (GetAttr(nsGkAtoms::placeholder, value)) {
|
|
ProcessPlaceholder(value, IsTextArea());
|
|
RefPtr ph =
|
|
MakePlaceholderOrPreview(doc, PseudoStyleType::Placeholder, value);
|
|
aShadow.AppendChildTo(ph, 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 {
|
|
nsTextControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
|
|
return frame ? frame->GetButton() : nullptr;
|
|
}
|
|
|
|
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,
|
|
AxisScrollParams(), AxisScrollParams(), flags);
|
|
}
|
|
|
|
void TextControlElement::ShowSelection() {
|
|
nsISelectionController* selCon = GetSelectionController();
|
|
if (!selCon) {
|
|
return;
|
|
}
|
|
RefPtr<Selection> ourSel =
|
|
selCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
|
|
if (!ourSel) {
|
|
return;
|
|
}
|
|
RefPtr<PresShell> ps = OwnerDoc()->GetPresShell();
|
|
if (!ps) {
|
|
return;
|
|
}
|
|
RefPtr<nsCaret> caret = ps->GetOriginalCaret();
|
|
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 (ps->IsDestroying()) {
|
|
return;
|
|
}
|
|
|
|
// 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::WillFocus(const WidgetEvent& aFocusEvent) {
|
|
MOZ_ASSERT(aFocusEvent.mMessage == eFocus);
|
|
MOZ_ASSERT(aFocusEvent.IsTrusted());
|
|
|
|
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.
|
|
const RefPtr<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();
|
|
}
|
|
}
|
|
if (fm && fm->GetFocusedElement() == this && aFocusEvent.IsTrusted())
|
|
[[likely]] {
|
|
const RefPtr<TextEditor> textEditor = GetExtantTextEditor();
|
|
if (textEditor) [[likely]] {
|
|
DebugOnly<nsresult> rv = textEditor->OnFocus(*this);
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"EditorBase::OnFocus() failed, but ignored");
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextControlElement::WillBlur(const WidgetEvent& aBlurEvent) {
|
|
MOZ_ASSERT(aBlurEvent.mMessage == eBlur);
|
|
|
|
if (aBlurEvent.IsTrusted()) {
|
|
const RefPtr<TextEditor> textEditor = GetExtantTextEditor();
|
|
if (textEditor) [[likely]] {
|
|
DebugOnly<nsresult> rv = textEditor->OnBlur(this);
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"EditorBase::OnBlur() failed, but ignored");
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextControlElement::SelectAll() {
|
|
if (auto* state = GetTextControlState()) {
|
|
state->SetSelectionRange(0, UINT32_MAX, Optional<nsAString>(),
|
|
IgnoreErrors(),
|
|
TextControlState::ScrollAfterSelection::No);
|
|
}
|
|
}
|
|
|
|
} // namespace mozilla
|