From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- layout/forms/nsTextControlFrame.cpp | 1325 +++++++++++++++++++++++++++++++++++ 1 file changed, 1325 insertions(+) create mode 100644 layout/forms/nsTextControlFrame.cpp (limited to 'layout/forms/nsTextControlFrame.cpp') diff --git a/layout/forms/nsTextControlFrame.cpp b/layout/forms/nsTextControlFrame.cpp new file mode 100644 index 0000000000..c51b94c56a --- /dev/null +++ b/layout/forms/nsTextControlFrame.cpp @@ -0,0 +1,1325 @@ +/* -*- 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 "mozilla/DebugOnly.h" + +#include "gfxContext.h" +#include "nsCOMPtr.h" +#include "nsFontMetrics.h" +#include "nsTextControlFrame.h" +#include "nsIEditor.h" +#include "nsCaret.h" +#include "nsCSSPseudoElements.h" +#include "nsDisplayList.h" +#include "nsGenericHTMLElement.h" +#include "nsTextFragment.h" +#include "nsNameSpaceManager.h" + +#include "nsIContent.h" +#include "nsIScrollableFrame.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsLayoutUtils.h" + +#include +#include "nsRange.h" //for selection setting helper func +#include "nsINode.h" +#include "nsPIDOMWindow.h" //needed for notify selection changed to update the menus ect. +#include "nsQueryObject.h" +#include "nsILayoutHistoryState.h" + +#include "nsFocusManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresState.h" +#include "mozilla/TextEditor.h" +#include "nsAttrValueInlines.h" +#include "mozilla/dom/Selection.h" +#include "nsContentUtils.h" +#include "nsTextNode.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/Text.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/Try.h" +#include "nsFrameSelection.h" + +#define DEFAULT_COLUMN_WIDTH 20 + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewTextControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) + nsTextControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsTextControlFrame) + +NS_QUERYFRAME_HEAD(nsTextControlFrame) + NS_QUERYFRAME_ENTRY(nsTextControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsITextControlFrame) + NS_QUERYFRAME_ENTRY(nsIStatefulFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsTextControlFrame::AccessibleType() { + return a11y::eHTMLTextFieldType; +} +#endif + +#ifdef DEBUG +class EditorInitializerEntryTracker { + public: + explicit EditorInitializerEntryTracker(nsTextControlFrame& frame) + : mFrame(frame), mFirstEntry(false) { + if (!mFrame.mInEditorInitialization) { + mFrame.mInEditorInitialization = true; + mFirstEntry = true; + } + } + ~EditorInitializerEntryTracker() { + if (mFirstEntry) { + mFrame.mInEditorInitialization = false; + } + } + bool EnteredMoreThanOnce() const { return !mFirstEntry; } + + private: + nsTextControlFrame& mFrame; + bool mFirstEntry; +}; +#endif + +class nsTextControlFrame::nsAnonDivObserver final + : public nsStubMutationObserver { + public: + explicit nsAnonDivObserver(nsTextControlFrame& aFrame) : mFrame(aFrame) {} + NS_DECL_ISUPPORTS + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + private: + ~nsAnonDivObserver() = default; + nsTextControlFrame& mFrame; +}; + +nsTextControlFrame::nsTextControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext, + nsIFrame::ClassID aClassID) + : nsContainerFrame(aStyle, aPresContext, aClassID) {} + +nsTextControlFrame::~nsTextControlFrame() = default; + +nsIScrollableFrame* nsTextControlFrame::GetScrollTargetFrame() const { + if (!mRootNode) { + return nullptr; + } + return do_QueryFrame(mRootNode->GetPrimaryFrame()); +} + +void nsTextControlFrame::Destroy(DestroyContext& aContext) { + RemoveProperty(TextControlInitializer()); + + // Unbind the text editor state object from the frame. The editor will live + // on, but things like controllers will be released. + RefPtr textControlElement = ControlElement(); + if (mMutationObserver) { + textControlElement->UnbindFromFrame(this); + mRootNode->RemoveMutationObserver(mMutationObserver); + mMutationObserver = nullptr; + } + + // If there is a drag session, user may be dragging selection in removing + // text node in the text control. If so, we should set source node to the + // text control because another text node may be recreated soon if the text + // control is just reframed. + if (nsCOMPtr dragSession = nsContentUtils::GetDragSession()) { + if (dragSession->IsDraggingTextInTextControl() && mRootNode && + mRootNode->GetFirstChild()) { + nsCOMPtr sourceNode; + if (NS_SUCCEEDED( + dragSession->GetSourceNode(getter_AddRefs(sourceNode))) && + mRootNode->Contains(sourceNode)) { + MOZ_ASSERT(sourceNode->IsText()); + dragSession->UpdateSource(textControlElement, nullptr); + } + } + } + // Otherwise, EventStateManager may track gesture to start drag with native + // anonymous nodes in the text control element. + else if (textControlElement->GetPresContext(Element::eForComposedDoc)) { + textControlElement->GetPresContext(Element::eForComposedDoc) + ->EventStateManager() + ->TextControlRootWillBeRemoved(*textControlElement); + } + + // If we're a subclass like nsNumberControlFrame, then it owns the root of the + // anonymous subtree where mRootNode is. + aContext.AddAnonymousContent(mRootNode.forget()); + aContext.AddAnonymousContent(mPlaceholderDiv.forget()); + aContext.AddAnonymousContent(mPreviewDiv.forget()); + aContext.AddAnonymousContent(mRevealButton.forget()); + + nsContainerFrame::Destroy(aContext); +} + +LogicalSize nsTextControlFrame::CalcIntrinsicSize( + gfxContext* aRenderingContext, WritingMode aWM, + float aFontSizeInflation) const { + LogicalSize intrinsicSize(aWM); + RefPtr fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); + const nscoord lineHeight = + ReflowInput::CalcLineHeight(*Style(), PresContext(), GetContent(), + NS_UNCONSTRAINEDSIZE, aFontSizeInflation); + // Use the larger of the font's "average" char width or the width of the + // zero glyph (if present) as the basis for resolving the size attribute. + const nscoord charWidth = + std::max(fontMet->ZeroOrAveCharWidth(), fontMet->AveCharWidth()); + const nscoord charMaxAdvance = fontMet->MaxAdvance(); + + // Initialize based on the width in characters. + const int32_t cols = GetCols(); + intrinsicSize.ISize(aWM) = cols * charWidth; + + // If we do not have what appears to be a fixed-width font, add a "slop" + // amount based on the max advance of the font (clamped to twice charWidth, + // because some fonts have a few extremely-wide outliers that would result + // in excessive width here; e.g. the triple-emdash ligature in SFNS Text), + // minus 4px. This helps avoid input fields becoming unusably narrow with + // small size values. + if (charMaxAdvance - charWidth > AppUnitsPerCSSPixel()) { + nscoord internalPadding = + std::max(0, std::min(charMaxAdvance, charWidth * 2) - + nsPresContext::CSSPixelsToAppUnits(4)); + internalPadding = RoundToMultiple(internalPadding, AppUnitsPerCSSPixel()); + intrinsicSize.ISize(aWM) += internalPadding; + } else { + // This is to account for the anonymous
having a 1 twip width + // in Full Standards mode, see BRFrame::Reflow and bug 228752. + if (PresContext()->CompatibilityMode() == eCompatibility_FullStandards) { + intrinsicSize.ISize(aWM) += 1; + } + } + + // Increment width with cols * letter-spacing. + { + const StyleLength& letterSpacing = StyleText()->mLetterSpacing; + if (!letterSpacing.IsZero()) { + intrinsicSize.ISize(aWM) += cols * letterSpacing.ToAppUnits(); + } + } + + // Set the height equal to total number of rows (times the height of each + // line, of course) + intrinsicSize.BSize(aWM) = lineHeight * GetRows(); + + // Add in the size of the scrollbars for textarea + if (IsTextArea()) { + nsIScrollableFrame* scrollableFrame = GetScrollTargetFrame(); + NS_ASSERTION(scrollableFrame, "Child must be scrollable"); + if (scrollableFrame) { + LogicalMargin scrollbarSizes(aWM, + scrollableFrame->GetDesiredScrollbarSizes()); + intrinsicSize.ISize(aWM) += scrollbarSizes.IStartEnd(aWM); + intrinsicSize.BSize(aWM) += scrollbarSizes.BStartEnd(aWM); + } + } + return intrinsicSize; +} + +nsresult nsTextControlFrame::EnsureEditorInitialized() { + // This method initializes our editor, if needed. + + // This code used to be called from CreateAnonymousContent(), but + // when the editor set the initial string, it would trigger a + // PresShell listener which called FlushPendingNotifications() + // during frame construction. This was causing other form controls + // to display wrong values. Additionally, calling this every time + // a text frame control is instantiated means that we're effectively + // instantiating the editor for all text fields, even if they + // never get used. So, now this method is being called lazily only + // when we actually need an editor. + + if (mEditorHasBeenInitialized) return NS_OK; + + Document* doc = mContent->GetComposedDoc(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + AutoWeakFrame weakFrame(this); + + // Flush out content on our document. Have to do this, because script + // blockers don't prevent the sink flushing out content and notifying in the + // process, which can destroy frames. + doc->FlushPendingNotifications(FlushType::ContentAndNotify); + NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_ERROR_FAILURE); + + // Make sure that editor init doesn't do things that would kill us off + // (especially off the script blockers it'll create for its DOM mutations). + { + RefPtr textControlElement = ControlElement(); + + // Hide selection changes during the initialization, as webpages should not + // be aware of these initializations + AutoHideSelectionChanges hideSelectionChanges( + textControlElement->GetConstFrameSelection()); + + nsAutoScriptBlocker scriptBlocker; + + // Time to mess with our security context... See comments in GetValue() + // for why this is needed. + mozilla::dom::AutoNoJSAPI nojsapi; + + // Make sure that we try to focus the content even if the method fails + class EnsureSetFocus { + public: + explicit EnsureSetFocus(nsTextControlFrame* aFrame) : mFrame(aFrame) {} + ~EnsureSetFocus() { + if (nsContentUtils::IsFocusedContent(mFrame->GetContent())) + mFrame->SetFocus(true, false); + } + + private: + nsTextControlFrame* mFrame; + }; + EnsureSetFocus makeSureSetFocusHappens(this); + +#ifdef DEBUG + // Make sure we are not being called again until we're finished. + // If reentrancy happens, just pretend that we don't have an editor. + const EditorInitializerEntryTracker tracker(*this); + NS_ASSERTION(!tracker.EnteredMoreThanOnce(), + "EnsureEditorInitialized has been called while a previous " + "call was in progress"); +#endif + + // Create an editor for the frame, if one doesn't already exist + nsresult rv = textControlElement->CreateEditor(); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(weakFrame.IsAlive()); + + // Set mEditorHasBeenInitialized so that subsequent calls will use the + // editor. + mEditorHasBeenInitialized = true; + + if (weakFrame.IsAlive()) { + uint32_t position = 0; + + // Set the selection to the end of the text field (bug 1287655), + // but only if the contents has changed (bug 1337392). + if (textControlElement->ValueChanged()) { + nsAutoString val; + textControlElement->GetTextEditorValue(val); + position = val.Length(); + } + + SetSelectionEndPoints(position, position); + } + } + NS_ENSURE_STATE(weakFrame.IsAlive()); + return NS_OK; +} + +already_AddRefed nsTextControlFrame::MakeAnonElement( + PseudoStyleType aPseudoType, Element* aParent, nsAtom* aTag) const { + MOZ_ASSERT(aPseudoType != PseudoStyleType::NotPseudo); + Document* doc = PresContext()->Document(); + RefPtr element = doc->CreateHTMLElement(aTag); + element->SetPseudoElementType(aPseudoType); + if (aPseudoType == PseudoStyleType::mozTextControlEditingRoot) { + // Make our root node editable + element->SetFlags(NODE_IS_EDITABLE); + } + + if (aPseudoType == PseudoStyleType::mozNumberSpinDown || + aPseudoType == PseudoStyleType::mozNumberSpinUp) { + element->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, u"true"_ns, + false); + } + + if (aParent) { + aParent->AppendChildTo(element, false, IgnoreErrors()); + } + + return element.forget(); +} + +already_AddRefed nsTextControlFrame::MakeAnonDivWithTextNode( + PseudoStyleType aPseudoType) const { + RefPtr div = MakeAnonElement(aPseudoType); + + // Create the text node for the anonymous
element. + nsNodeInfoManager* nim = div->OwnerDoc()->NodeInfoManager(); + RefPtr textNode = new (nim) nsTextNode(nim); + // If the anonymous div element is not for the placeholder, we should + // mark the text node as "maybe modified frequently" for avoiding ASCII + // range checks at every input. + if (aPseudoType != PseudoStyleType::placeholder) { + textNode->MarkAsMaybeModifiedFrequently(); + // Additionally, this is a password field, the text node needs to be + // marked as "maybe masked" unless it's in placeholder. + if (IsPasswordTextControl()) { + textNode->MarkAsMaybeMasked(); + } + } + div->AppendChildTo(textNode, false, IgnoreErrors()); + return div.forget(); +} + +nsresult nsTextControlFrame::CreateAnonymousContent( + nsTArray& aElements) { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript()); + MOZ_ASSERT(mContent, "We should have a content!"); + + AddStateBits(NS_FRAME_INDEPENDENT_SELECTION); + + RefPtr textControlElement = ControlElement(); + mRootNode = MakeAnonElement(PseudoStyleType::mozTextControlEditingRoot); + if (NS_WARN_IF(!mRootNode)) { + return NS_ERROR_FAILURE; + } + + mMutationObserver = new nsAnonDivObserver(*this); + mRootNode->AddMutationObserver(mMutationObserver); + + // Bind the frame to its text control. + // + // This can realistically fail in paginated mode, where we may replicate + // fixed-positioned elements and the replicated frame will not get the chance + // to get an editor. + nsresult rv = textControlElement->BindToFrame(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mRootNode->RemoveMutationObserver(mMutationObserver); + mMutationObserver = nullptr; + mRootNode = nullptr; + return rv; + } + + CreatePlaceholderIfNeeded(); + if (mPlaceholderDiv) { + aElements.AppendElement(mPlaceholderDiv); + } + CreatePreviewIfNeeded(); + if (mPreviewDiv) { + aElements.AppendElement(mPreviewDiv); + } + + // NOTE(emilio): We want the root node always after the placeholder so that + // background on the placeholder doesn't obscure the caret. + aElements.AppendElement(mRootNode); + + if (StaticPrefs::layout_forms_reveal_password_button_enabled() && + IsPasswordTextControl()) { + mRevealButton = + MakeAnonElement(PseudoStyleType::mozReveal, nullptr, nsGkAtoms::button); + mRevealButton->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, + u"true"_ns, false); + mRevealButton->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns, + false); + aElements.AppendElement(mRevealButton); + } + + rv = UpdateValueDisplay(false); + NS_ENSURE_SUCCESS(rv, rv); + + InitializeEagerlyIfNeeded(); + return NS_OK; +} + +bool nsTextControlFrame::ShouldInitializeEagerly() const { + // textareas are eagerly initialized. + if (!IsSingleLineTextControl()) { + return true; + } + + // Also, input elements which have a cached selection should get eager + // editor initialization. + TextControlElement* textControlElement = ControlElement(); + if (textControlElement->HasCachedSelection()) { + return true; + } + + // So do input text controls with spellcheck=true + if (auto* htmlElement = nsGenericHTMLElement::FromNode(mContent)) { + if (htmlElement->Spellcheck()) { + return true; + } + } + + // If text in the editor is being dragged, we need the editor to create + // new source node for the drag session (TextEditor creates the text node + // in the anonymous
element. + if (nsCOMPtr dragSession = nsContentUtils::GetDragSession()) { + if (dragSession->IsDraggingTextInTextControl()) { + nsCOMPtr sourceNode; + if (NS_SUCCEEDED( + dragSession->GetSourceNode(getter_AddRefs(sourceNode))) && + sourceNode == textControlElement) { + return true; + } + } + } + + return false; +} + +void nsTextControlFrame::InitializeEagerlyIfNeeded() { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript(), + "Someone forgot a script blocker?"); + if (!ShouldInitializeEagerly()) { + return; + } + + EditorInitializer* initializer = new EditorInitializer(this); + SetProperty(TextControlInitializer(), initializer); + nsContentUtils::AddScriptRunner(initializer); +} + +void nsTextControlFrame::CreatePlaceholderIfNeeded() { + MOZ_ASSERT(!mPlaceholderDiv); + + // Do we need a placeholder node? + nsAutoString placeholder; + if (!mContent->AsElement()->GetAttr(nsGkAtoms::placeholder, placeholder)) { + return; + } + + mPlaceholderDiv = MakeAnonDivWithTextNode(PseudoStyleType::placeholder); + UpdatePlaceholderText(placeholder, false); +} + +void nsTextControlFrame::PlaceholderChanged(const nsAttrValue* aOld, + const nsAttrValue* aNew) { + if (!aOld || !aNew) { + return; // This should be handled by GetAttributeChangeHint. + } + + // If we've changed the attribute but we still haven't reframed, there's + // nothing to do either. + if (!mPlaceholderDiv) { + return; + } + + nsAutoString placeholder; + aNew->ToString(placeholder); + UpdatePlaceholderText(placeholder, true); +} + +void nsTextControlFrame::UpdatePlaceholderText(nsString& aPlaceholder, + bool aNotify) { + MOZ_DIAGNOSTIC_ASSERT(mPlaceholderDiv); + MOZ_DIAGNOSTIC_ASSERT(mPlaceholderDiv->GetFirstChild()); + + if (IsTextArea()) { //