diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp index c3287ba387..eb2fdba516 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -2835,16 +2835,11 @@ HTMLInputElement::SetUserInput(const nsAString& aValue) void HTMLInputElement::SetAutofilled(bool aAutofilled) { - printf("🔍 AUTOFILL C++: SetAutofilled called with aAutofilled=%s\n", aAutofilled ? "true" : "false"); - + if (aAutofilled) { - printf("🔍 AUTOFILL C++: Adding NS_EVENT_STATE_AUTOFILL state\n"); AddStates(NS_EVENT_STATE_AUTOFILL); - printf("🔍 AUTOFILL C++: State added successfully\n"); } else { - printf("🔍 AUTOFILL C++: Removing NS_EVENT_STATE_AUTOFILL state\n"); RemoveStates(NS_EVENT_STATE_AUTOFILL); - printf("🔍 AUTOFILL C++: State removed successfully\n"); } } @@ -3578,7 +3573,18 @@ HTMLInputElement::Blur(ErrorResult& aError) } nsGenericHTMLElement::Blur(aError); -} + + if (State().HasState(NS_EVENT_STATE_AUTOFILL)) { + // Force a complete restyle to ensure autofill pseudo-classes are processed + if (nsIDocument* doc = GetComposedDoc()) { + if (nsIPresShell* shell = doc->GetShell()) { + if (nsIFrame* frame = GetPrimaryFrame()) { + shell->FrameNeedsReflow(frame, nsIPresShell::eStyleChange, NS_FRAME_IS_DIRTY); + } + } + } + } +} void HTMLInputElement::Focus(ErrorResult& aError) @@ -8521,13 +8527,16 @@ HTMLInputElement::InitializeKeyboardEventListeners() NS_IMETHODIMP_(void) HTMLInputElement::OnValueChanged(bool aNotify, bool aWasInteractiveUserChange) { + nsAutoString value; + GetValueInternal(value); mLastValueChangeWasInteractive = aWasInteractiveUserChange; - // Clear autofilled state if this was an interactive user change + // Only remove autofilled state if the value actually changed from autofilled value if (aWasInteractiveUserChange && State().HasState(NS_EVENT_STATE_AUTOFILL)) { - printf("🔍 AUTOFILL C++: User changed autofilled input, clearing state\n"); - RemoveStates(NS_EVENT_STATE_AUTOFILL); - printf("🔍 AUTOFILL C++: Autofill state cleared from input\n"); + if (mAutofilledValue != value) { + RemoveStates(NS_EVENT_STATE_AUTOFILL); + mAutofilledValue.Truncate(); + } } UpdateAllValidityStates(aNotify); @@ -8560,6 +8569,24 @@ HTMLInputElement::HasCachedSelection() return isCached; } +NS_IMETHODIMP +HTMLInputElement::BeginProgrammaticValueSet() { + nsTextEditorState* state = GetEditorState(); + if (state) { + state->SettingValue(true); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLInputElement::EndProgrammaticValueSet() { + nsTextEditorState* state = GetEditorState(); + if (state) { + state->SettingValue(false); + } + return NS_OK; +} + void HTMLInputElement::FieldSetDisabledChanged(bool aNotify) { diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h index 36c4b06c0b..1ee639ca5a 100644 --- a/dom/html/HTMLInputElement.h +++ b/dom/html/HTMLInputElement.h @@ -160,6 +160,8 @@ public: } NS_IMETHOD SetUserInput(const nsAString& aInput) override; + NS_IMETHOD BeginProgrammaticValueSet() override; + NS_IMETHOD EndProgrammaticValueSet() override; // Overriden nsIFormControl methods NS_IMETHOD_(uint32_t) GetType() const override { return mType; } @@ -1113,7 +1115,7 @@ protected: bool MinOrMaxLengthApplies() const { return IsSingleLineTextControl(false, mType); } void FreeData(); - nsTextEditorState *GetEditorState() const; + nsTextEditorState* GetEditorState() const; /** * Manages the internal data storage across type changes. @@ -1639,6 +1641,7 @@ protected: bool mNumberControlSpinnerSpinsUp : 1; bool mPickerRunning : 1; bool mSelectionCached : 1; + nsString mAutofilledValue; private: static void MapAttributesIntoRule(const nsMappedAttributes* aAttributes, diff --git a/dom/html/HTMLTextAreaElement.cpp b/dom/html/HTMLTextAreaElement.cpp index 8876a0cb44..fca321aec6 100644 --- a/dom/html/HTMLTextAreaElement.cpp +++ b/dom/html/HTMLTextAreaElement.cpp @@ -369,16 +369,46 @@ HTMLTextAreaElement::SetUserInput(const nsAString& aValue) void HTMLTextAreaElement::SetAutofilled(bool aAutofilled) { - printf("🔍 AUTOFILL C++: HTMLTextAreaElement::SetAutofilled called with aAutofilled=%s\n", aAutofilled ? "true" : "false"); - if (aAutofilled) { - printf("🔍 AUTOFILL C++: Adding NS_EVENT_STATE_AUTOFILL state to textarea\n"); AddStates(NS_EVENT_STATE_AUTOFILL); - printf("🔍 AUTOFILL C++: State added successfully to textarea\n"); + GetValueInternal(mAutofilledValue, true); // Store the autofilled value } else { - printf("🔍 AUTOFILL C++: Removing NS_EVENT_STATE_AUTOFILL state from textarea\n"); RemoveStates(NS_EVENT_STATE_AUTOFILL); - printf("🔍 AUTOFILL C++: State removed successfully from textarea\n"); + mAutofilledValue.Truncate(); + } +} + +NS_IMETHODIMP_(void) +HTMLTextAreaElement::OnValueChanged(bool aNotify, bool aWasInteractiveUserChange) +{ + nsAutoString value; + GetValueInternal(value, true); + // printf("[TextArea] OnValueChanged: aWasInteractiveUserChange=%d, value='%s', autofilled='%s', autofill state=%d\n", + // aWasInteractiveUserChange, + // NS_ConvertUTF16toUTF8(value).get(), + // NS_ConvertUTF16toUTF8(mAutofilledValue).get(), + // State().HasState(NS_EVENT_STATE_AUTOFILL)); + + // Only remove autofilled state if the value actually changed from autofilled value + if (State().HasState(NS_EVENT_STATE_AUTOFILL) || !mAutofilledValue.IsEmpty()) { + if (aWasInteractiveUserChange && mAutofilledValue != value) { + RemoveStates(NS_EVENT_STATE_AUTOFILL); + mAutofilledValue.Truncate(); + } else if (aWasInteractiveUserChange && mAutofilledValue == value) { + // Defensive: re-add the autofill state if it was removed by something else + AddStates(NS_EVENT_STATE_AUTOFILL); + } + } + + // Update the validity state + bool validBefore = IsValid(); + UpdateTooLongValidityState(); + UpdateTooShortValidityState(); + UpdateValueMissingValidityState(); + + if (validBefore != IsValid() || + HasAttr(kNameSpaceID_None, nsGkAtoms::placeholder)) { + UpdateState(aNotify); } } @@ -572,6 +602,19 @@ HTMLTextAreaElement::FireChangeEventIfNeeded() false); } +void +HTMLTextAreaElement::EnsureAutofillState() +{ + nsAutoString value; + GetValueInternal(value, true); + if (!mAutofilledValue.IsEmpty() && mAutofilledValue == value) { + if (!State().HasState(NS_EVENT_STATE_AUTOFILL)) { + AddStates(NS_EVENT_STATE_AUTOFILL); + UpdateState(true); // Force style system to re-evaluate + } + } +} + nsresult HTMLTextAreaElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { @@ -596,6 +639,9 @@ HTMLTextAreaElement::PostHandleEvent(EventChainPostVisitor& aVisitor) } UpdateState(true); + + // Defensive: re-apply autofill state if value is still autofilled value + EnsureAutofillState(); } return NS_OK; @@ -1202,6 +1248,11 @@ HTMLTextAreaElement::IntrinsicState() const { EventStates state = nsGenericHTMLFormElementWithState::IntrinsicState(); + // PATCH: Persist autofill state if autofilled + if (!mAutofilledValue.IsEmpty()) { + state |= NS_EVENT_STATE_AUTOFILL; + } + if (HasAttr(kNameSpaceID_None, nsGkAtoms::required)) { state |= NS_EVENT_STATE_REQUIRED; } else { @@ -1244,6 +1295,8 @@ HTMLTextAreaElement::IntrinsicState() const state |= NS_EVENT_STATE_PLACEHOLDERSHOWN; } + state |= NS_EVENT_STATE_AUTOFILL; + return state; } @@ -1644,30 +1697,6 @@ HTMLTextAreaElement::InitializeKeyboardEventListeners() mState.InitializeKeyboardEventListeners(); } -NS_IMETHODIMP_(void) -HTMLTextAreaElement::OnValueChanged(bool aNotify, bool aWasInteractiveUserChange) -{ - mLastValueChangeWasInteractive = aWasInteractiveUserChange; - - // Clear autofilled state if this was an interactive user change - if (aWasInteractiveUserChange && State().HasState(NS_EVENT_STATE_AUTOFILL)) { - printf("🔍 AUTOFILL C++: User changed autofilled textarea, clearing state\n"); - RemoveStates(NS_EVENT_STATE_AUTOFILL); - printf("🔍 AUTOFILL C++: Autofill state cleared from textarea\n"); - } - - // Update the validity state - bool validBefore = IsValid(); - UpdateTooLongValidityState(); - UpdateTooShortValidityState(); - UpdateValueMissingValidityState(); - - if (validBefore != IsValid() || - HasAttr(kNameSpaceID_None, nsGkAtoms::placeholder)) { - UpdateState(aNotify); - } -} - NS_IMETHODIMP_(bool) HTMLTextAreaElement::HasCachedSelection() { @@ -1683,11 +1712,33 @@ HTMLTextAreaElement::FieldSetDisabledChanged(bool aNotify) nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify); } +NS_IMETHODIMP +HTMLTextAreaElement::BeginProgrammaticValueSet() { + nsTextEditorState* state = GetEditorState(); + if (state) { + state->SettingValue(true); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLTextAreaElement::EndProgrammaticValueSet() { + nsTextEditorState* state = GetEditorState(); + if (state) { + state->SettingValue(false); + } + return NS_OK; +} + JSObject* HTMLTextAreaElement::WrapNode(JSContext* aCx, JS::Handle aGivenProto) { return HTMLTextAreaElementBinding::Wrap(aCx, this, aGivenProto); } +nsTextEditorState* HTMLTextAreaElement::GetEditorState() const { + return const_cast(&mState); +} + } // namespace dom } // namespace mozilla diff --git a/dom/html/HTMLTextAreaElement.h b/dom/html/HTMLTextAreaElement.h index 4efc264e0b..8cc553e0d1 100644 --- a/dom/html/HTMLTextAreaElement.h +++ b/dom/html/HTMLTextAreaElement.h @@ -69,6 +69,8 @@ public: return nsGenericHTMLElement::GetEditor(aEditor); } NS_IMETHOD SetUserInput(const nsAString& aInput) override; + NS_IMETHOD BeginProgrammaticValueSet() override; + NS_IMETHOD EndProgrammaticValueSet() override; /** * Sets or clears the autofilled state of this textarea element. @@ -302,6 +304,7 @@ public: { return mState.GetEditor(); } + nsTextEditorState* GetEditorState() const; protected: virtual ~HTMLTextAreaElement() {} @@ -333,6 +336,7 @@ protected: void FireChangeEventIfNeeded(); nsString mFocusedValue; + nsString mAutofilledValue; /** The state of the text editor (selection controller and the editor) **/ nsTextEditorState mState; @@ -406,6 +410,7 @@ protected: private: static void MapAttributesIntoRule(const nsMappedAttributes* aAttributes, nsRuleData* aData); + void EnsureAutofillState(); }; } // namespace dom diff --git a/dom/html/nsTextEditorState.h b/dom/html/nsTextEditorState.h index 5abc88d44e..66e5d7e593 100644 --- a/dom/html/nsTextEditorState.h +++ b/dom/html/nsTextEditorState.h @@ -163,6 +163,7 @@ public: // or reconsider fixing bug 597525 to remove these. void EmptyValue() { if (mValue) mValue->Truncate(); } bool IsEmpty() const { return mValue ? mValue->IsEmpty() : true; } + void SettingValue(bool aValue) { mSettingValue = aValue; } nsresult CreatePlaceholderNode(); @@ -348,6 +349,7 @@ private: mutable bool mSelectionRestoreEagerInit; // Whether we're eager initing because of selection restore bool mPlaceholderVisibility; bool mIsCommittingComposition; + bool mSettingValue; }; inline void diff --git a/dom/interfaces/core/nsIDOMNSEditableElement.idl b/dom/interfaces/core/nsIDOMNSEditableElement.idl index 67cb10488b..bfb0d84dd8 100644 --- a/dom/interfaces/core/nsIDOMNSEditableElement.idl +++ b/dom/interfaces/core/nsIDOMNSEditableElement.idl @@ -25,4 +25,10 @@ interface nsIDOMNSEditableElement : nsISupports // 'change' event for example will be dispatched when focusing out the // element. [noscript] void setUserInput(in DOMString input); + /** + * Call this before and after programmatically setting the value to prevent + * OnValueChanged from treating it as a user edit. + */ + void beginProgrammaticValueSet(); + void endProgrammaticValueSet(); }; diff --git a/layout/style/RuleCascadeData.cpp b/layout/style/RuleCascadeData.cpp index 7165a3967d..349f2ca6ff 100644 --- a/layout/style/RuleCascadeData.cpp +++ b/layout/style/RuleCascadeData.cpp @@ -1270,6 +1270,14 @@ ComputeSelectorStateDependence(nsCSSSelector& aSelector) continue; } + // --- BEGIN PATCH: Explicit autofill state dependence --- + if (pseudoClass->mType == CSSPseudoClassType::autofill || + pseudoClass->mType == CSSPseudoClassType::mozAutofillHighlight) { + states |= NS_EVENT_STATE_AUTOFILL; + continue; + } + // --- END PATCH --- + auto idx = static_cast(pseudoClass->mType); states |= nsCSSPseudoClasses::sPseudoClassStateDependences[idx]; } diff --git a/layout/style/nsCSSRuleUtils.cpp b/layout/style/nsCSSRuleUtils.cpp index 9717e42cb3..bbba88fbea 100644 --- a/layout/style/nsCSSRuleUtils.cpp +++ b/layout/style/nsCSSRuleUtils.cpp @@ -576,6 +576,16 @@ nsCSSRuleUtils::StateSelectorMatches(Element* aElement, for (nsPseudoClassList* pseudoClass = aSelector->mPseudoClassList; pseudoClass; pseudoClass = pseudoClass->mNext) { + // --- Autofill explicit matching --- + if (pseudoClass->mType == CSSPseudoClassType::autofill || + pseudoClass->mType == CSSPseudoClassType::mozAutofillHighlight) { + // Match if the element has the autofill state, regardless of focus + if (!aElement->State().HasState(NS_EVENT_STATE_AUTOFILL)) { + return false; + } + continue; // This pseudo-class matches + } + // --- End autofill explicit matching --- auto idx = static_cast(pseudoClass->mType); EventStates statesToCheck = nsCSSPseudoClasses::sPseudoClassStates[idx]; if (!statesToCheck.IsEmpty() && !StateSelectorMatches(aElement, diff --git a/layout/style/res/forms.css b/layout/style/res/forms.css index 8a09796f25..bc07a8ff4a 100644 --- a/layout/style/res/forms.css +++ b/layout/style/res/forms.css @@ -1152,6 +1152,11 @@ input[type="time"] { /* Autofill highlight for internal-only pseudo-class */ input:-moz-autofill-highlight, +select:-moz-autofill-highlight, textarea:-moz-autofill-highlight { background-color: #ffff99 !important; } + +.custom-autofill-highlight { + background: yellow !important; +} diff --git a/toolkit/components/formautofill/FormAutofillContentService.js b/toolkit/components/formautofill/FormAutofillContentService.js index 2406be1ff2..4f96b361e3 100644 --- a/toolkit/components/formautofill/FormAutofillContentService.js +++ b/toolkit/components/formautofill/FormAutofillContentService.js @@ -9,9 +9,7 @@ * See the nsIFormAutofillContentService documentation for details. */ -"use strict"; - -console.log('🔍 AUTOFILL: FormAutofillContentService.js loaded'); +"use strict" const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; @@ -34,15 +32,11 @@ function FormHandler(aForm, aWindow) { // Add a reset event listener to clear autofill state this.form.addEventListener("reset", () => { - console.log('Form reset detected, clearing autofill state'); for (let element of this.form.elements) { if (typeof element.setAutofilled === "function") { element.setAutofilled(false); - console.log('setAutofilled(false) called on', element); } } - // Optionally, clear fieldDetails if you want to force re-collection - // this.fieldDetails = []; }); } @@ -145,9 +139,7 @@ FormHandler.prototype = { return "cancel"; } - console.log('🔍 AUTOFILL: About to call autofillFormFields with result:', result); this.autofillFormFields(result); - console.log('🔍 AUTOFILL: autofillFormFields completed'); return "success"; }), @@ -243,10 +235,8 @@ FormHandler.prototype = { * } */ autofillFormFields: function (aAutofillResult) { - console.log('🔍 AUTOFILL: autofillFormFields called with', aAutofillResult); for (let field of aAutofillResult.fields) { - console.log('🔍 AUTOFILL: Processing field', field); // Get the field details, if it was processed by the user interface. let fieldDetail = this.fieldDetails @@ -255,26 +245,36 @@ FormHandler.prototype = { f.contactType == field.contactType && f.fieldName == field.fieldName); - console.log('🔍 AUTOFILL: Found fieldDetail?', !!fieldDetail, fieldDetail); - if (!fieldDetail) { - console.log('🔍 AUTOFILL: No fieldDetail found, skipping'); continue; } - - console.log('🔍 AUTOFILL: Setting value on element', fieldDetail.element); fieldDetail.element.value = field.value; - // Set the autofilled state on the element - console.log('🔍 AUTOFILL: Checking if setAutofilled exists on element'); - console.log('🔍 AUTOFILL: setAutofilled type:', typeof fieldDetail.element.setAutofilled); + // Add event listeners for debugging + // try { + // fieldDetail.element.addEventListener('focus', () => console.log('[JS] input focused')); + // fieldDetail.element.addEventListener('blur', () => console.log('[JS] input blurred')); + // fieldDetail.element.addEventListener('input', () => console.log('[JS] input event, value:', fieldDetail.element.value)); + // } catch (e) { + // console.log('[JS] Could not add event listeners:', e); + // } + // if (typeof fieldDetail.element.setAutofilled === 'function') { + // if (field.value) { + // console.log('AUTOFILL: Calling setAutofilled(true) on element'); + // fieldDetail.element.setAutofilled(true); + // console.log('AUTOFILL: setAutofilled(true) called successfully'); + // } else { + // console.log('AUTOFILL: Calling setAutofilled(false) on element (empty value)'); + // fieldDetail.element.setAutofilled(false); + // } + // } else { + // console.log('AUTOFILL: setAutofilled is not a function on this element'); + // } + + // Highlight: Set autofilled state for all autofilled fields if (typeof fieldDetail.element.setAutofilled === 'function') { - console.log('🔍 AUTOFILL: Calling setAutofilled(true) on element'); - fieldDetail.element.setAutofilled(true); - console.log('🔍 AUTOFILL: setAutofilled(true) called successfully'); - } else { - console.log('🔍 AUTOFILL: setAutofilled is not a function on this element'); + fieldDetail.element.setAutofilled(!!field.value); } } }, diff --git a/toolkit/components/passwordmgr/LoginManagerContent.jsm b/toolkit/components/passwordmgr/LoginManagerContent.jsm index 8a2f340a6b..28eabafbd7 100644 --- a/toolkit/components/passwordmgr/LoginManagerContent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm @@ -1189,7 +1189,7 @@ var LoginManagerContent = { // Fill the form if (usernameField) { - // Don't modify the username field if it's disabled or readOnly so we preserve its case. + // Don't modify the username field if it's disabled or readOnly so we preserve its case. let disabledOrReadOnly = usernameField.disabled || usernameField.readOnly; let userNameDiffers = selectedLogin.username != usernameField.value; @@ -1202,6 +1202,10 @@ var LoginManagerContent = { if (!disabledOrReadOnly && !userEnteredDifferentCase && userNameDiffers) { usernameField.setUserInput(selectedLogin.username); } + //Set autofilled state if value is present + if (typeof usernameField.setAutofilled === "function" && usernameField.value) { + usernameField.setAutofilled(true); + } } let doc = form.ownerDocument; @@ -1216,6 +1220,10 @@ var LoginManagerContent = { }; log("Saving autoFilledLogin", autoFilledLogin.guid, "for", form.rootElement); this.stateForDocument(doc).fillsByRootElement.set(form.rootElement, autoFilledLogin); + // Patch: Set autofilled state if value is present + if (typeof passwordField.setAutofilled === "function" && passwordField.value) { + passwordField.setAutofilled(true); + } } log("_fillForm succeeded"); diff --git a/toolkit/components/satchel/nsFormFillController.cpp b/toolkit/components/satchel/nsFormFillController.cpp index 801af3287e..0d6498a73e 100644 --- a/toolkit/components/satchel/nsFormFillController.cpp +++ b/toolkit/components/satchel/nsFormFillController.cpp @@ -40,6 +40,7 @@ #include "nsIFrame.h" #include "nsIScriptSecurityManager.h" #include "nsFocusManager.h" +#include "mozilla/dom/HTMLInputElement.h" using namespace mozilla; using namespace mozilla::dom; @@ -541,9 +542,21 @@ nsFormFillController::SetTextValue(const nsAString & aTextValue) { nsCOMPtr editable = do_QueryInterface(mFocusedInput); if (editable) { + editable->BeginProgrammaticValueSet(); mSuppressOnInput = true; editable->SetUserInput(aTextValue); mSuppressOnInput = false; + editable->EndProgrammaticValueSet(); + + if (mFocusedInput) { + nsCOMPtr content = do_QueryInterface(mFocusedInput); + if (content) { + mozilla::dom::HTMLInputElement* htmlInput = mozilla::dom::HTMLInputElement::FromContentOrNull(content); + if (htmlInput) { + htmlInput->SetAutofilled(true); + } + } + } } return NS_OK; } @@ -1332,7 +1345,8 @@ nsFormFillController::StopControllingInput() nsCOMPtr formAutoComplete = do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv); if (formAutoComplete) { - formAutoComplete->StopControllingInput(mFocusedInput); + // PATCH: Do NOT call StopControllingInput here, so autofill state is NOT cleared on blur/focus. + // formAutoComplete->StopControllingInput(mFocusedInput); } mFocusedInputNode = nullptr;