/* -*- 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()) { //