/* -*- 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 "nsIPlaintextEditor.h" #include "nsCaret.h" #include "nsCSSPseudoElements.h" #include "nsGenericHTMLElement.h" #include "nsIEditor.h" #include "nsTextFragment.h" #include "nsNameSpaceManager.h" #include "nsCheckboxRadioFrame.h" //for registering accesskeys #include "nsIContent.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/PresShell.h" #include "mozilla/PresState.h" #include "nsAttrValueInlines.h" #include "mozilla/dom/Selection.h" #include "mozilla/TextEditRules.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 "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(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() {} nsTextControlFrame& mFrame; }; nsTextControlFrame::nsTextControlFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) : nsContainerFrame(aStyle, aPresContext, kClassID), mFirstBaseline(NS_INTRINSIC_ISIZE_UNKNOWN), mEditorHasBeenInitialized(false), mIsProcessing(false) #ifdef DEBUG , mInEditorInitialization(false) #endif { ClearCachedValue(); } nsTextControlFrame::~nsTextControlFrame() {} void nsTextControlFrame::DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData) { mScrollEvent.Revoke(); DeleteProperty(TextControlInitializer()); // Unbind the text editor state object from the frame. The editor will live // on, but things like controllers will be released. nsCOMPtr txtCtrl = do_QueryInterface(GetContent()); NS_ASSERTION(txtCtrl, "Content not a text control element"); txtCtrl->UnbindFromFrame(this); nsCheckboxRadioFrame::RegUnRegAccessKey(static_cast(this), false); if (mMutationObserver) { mRootNode->RemoveMutationObserver(mMutationObserver); mMutationObserver = nullptr; } // FIXME(emilio, bug 1400618): Do this after the child frames are destroyed. aPostDestroyData.AddAnonymousContent(mRootNode.forget()); aPostDestroyData.AddAnonymousContent(mPlaceholderDiv.forget()); aPostDestroyData.AddAnonymousContent(mPreviewDiv.forget()); nsContainerFrame::DestroyFrom(aDestructRoot, aPostDestroyData); } LogicalSize nsTextControlFrame::CalcIntrinsicSize( gfxContext* aRenderingContext, WritingMode aWM, float aFontSizeInflation) const { LogicalSize intrinsicSize(aWM); // Get leading and the Average/MaxAdvance char width nscoord lineHeight = 0; nscoord charWidth = 0; nscoord charMaxAdvance = 0; RefPtr fontMet = nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); lineHeight = ReflowInput::CalcLineHeight(GetContent(), Style(), PresContext(), NS_AUTOHEIGHT, aFontSizeInflation); charWidth = fontMet->AveCharWidth(); charMaxAdvance = fontMet->MaxAdvance(); // Set the width equal to the width in characters int32_t cols = GetCols(); intrinsicSize.ISize(aWM) = cols * charWidth; // To better match IE, take the maximum character width(in twips) and remove // 4 pixels add this on as additional padding(internalPadding). But only do // this if we think we have a fixed-width font. if (mozilla::Abs(charWidth - charMaxAdvance) > (unsigned)nsPresContext::CSSPixelsToAppUnits(1)) { nscoord internalPadding = std::max(0, charMaxAdvance - nsPresContext::CSSPixelsToAppUnits(4)); nscoord t = nsPresContext::CSSPixelsToAppUnits(1); // Round to a multiple of t nscoord rest = internalPadding % t; if (rest < t - rest) { internalPadding -= rest; } else { internalPadding += t - rest; } // Now add the extra padding on (so that small input sizes work well) 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()) { nsIFrame* first = PrincipalChildList().FirstChild(); nsIScrollableFrame* scrollableFrame = do_QueryFrame(first); NS_ASSERTION(scrollableFrame, "Child must be scrollable"); if (scrollableFrame) { LogicalMargin scrollbarSizes( aWM, scrollableFrame->GetDesiredScrollbarSizes(PresContext(), aRenderingContext)); 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). { nsCOMPtr txtCtrl = do_QueryInterface(GetContent()); MOZ_ASSERT(txtCtrl, "Content not a text control element"); // Hide selection changes during the initialization, as webpages should not // be aware of these initializations AutoHideSelectionChanges hideSelectionChanges( txtCtrl->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 = txtCtrl->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 (txtCtrl->ValueChanged()) { nsAutoString val; txtCtrl->GetTextEditorValue(val, true); position = val.Length(); } SetSelectionEndPoints(position, position); } } NS_ENSURE_STATE(weakFrame.IsAlive()); return NS_OK; } static already_AddRefed CreateEmptyDiv( const nsTextControlFrame& aOwnerFrame) { Document* doc = aOwnerFrame.PresContext()->Document(); RefPtr nodeInfo = doc->NodeInfoManager()->GetNodeInfo( nsGkAtoms::div, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); RefPtr element = NS_NewHTMLDivElement(nodeInfo.forget()); return element.forget(); } static already_AddRefed CreateEmptyDivWithTextNode( const nsTextControlFrame& aOwnerFrame) { RefPtr element = CreateEmptyDiv(aOwnerFrame); // Create the text node for DIV RefPtr textNode = new nsTextNode(element->OwnerDoc()->NodeInfoManager()); textNode->MarkAsMaybeModifiedFrequently(); element->AppendChildTo(textNode, false); return element.forget(); } nsresult nsTextControlFrame::CreateAnonymousContent( nsTArray& aElements) { MOZ_ASSERT(mContent, "We should have a content!"); AddStateBits(NS_FRAME_INDEPENDENT_SELECTION); nsCOMPtr txtCtrl = do_QueryInterface(GetContent()); MOZ_ASSERT(txtCtrl, "Content not a text control element"); nsresult rv = CreateRootNode(); NS_ENSURE_SUCCESS(rv, rv); // Bind the frame to its text control rv = txtCtrl->BindToFrame(this); NS_ENSURE_SUCCESS(rv, rv); aElements.AppendElement(mRootNode); CreatePlaceholderIfNeeded(); if (mPlaceholderDiv) { if (!IsSingleLineTextControl()) { // For textareas, UpdateValueDisplay doesn't initialize the visibility // status of the placeholder because it returns early, so we have to // do that manually here. txtCtrl->UpdateOverlayTextVisibility(true); } aElements.AppendElement(mPlaceholderDiv); } CreatePreviewIfNeeded(); if (mPreviewDiv) { aElements.AppendElement(mPreviewDiv); } 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. nsCOMPtr txtCtrl = do_QueryInterface(GetContent()); if (txtCtrl->HasCachedSelection()) { return true; } // So do input text controls with spellcheck=true if (auto* htmlElement = nsGenericHTMLElement::FromNode(mContent)) { if (htmlElement->Spellcheck()) { 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); } nsresult nsTextControlFrame::CreateRootNode() { MOZ_ASSERT(!mRootNode); mRootNode = CreateEmptyDiv(*this); mRootNode->SetIsNativeAnonymousRoot(); mMutationObserver = new nsAnonDivObserver(*this); mRootNode->AddMutationObserver(mMutationObserver); // Make our root node editable mRootNode->SetFlags(NODE_IS_EDITABLE); // Set the necessary classes on the text control. We use class values instead // of a 'style' attribute so that the style comes from a user-agent style // sheet and is still applied even if author styles are disabled. nsAutoString classValue; classValue.AppendLiteral("anonymous-div"); if (!IsSingleLineTextControl()) { // We can't just inherit the overflow because setting visible overflow will // crash when the number of lines exceeds the height of the textarea and // setting -moz-hidden-unscrollable overflow doesn't paint the caret for // some reason. const nsStyleDisplay* disp = StyleDisplay(); if (disp->mOverflowX != StyleOverflow::Visible && disp->mOverflowX != StyleOverflow::MozHiddenUnscrollable) { classValue.AppendLiteral(" inherit-overflow"); } } nsresult rv = mRootNode->SetAttr(kNameSpaceID_None, nsGkAtoms::_class, classValue, false); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } void nsTextControlFrame::CreatePlaceholderIfNeeded() { MOZ_ASSERT(!mPlaceholderDiv); // Do we need a placeholder node? nsAutoString placeholderTxt; mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::placeholder, placeholderTxt); if (IsTextArea()) { //