summaryrefslogtreecommitdiffstats
path: root/dom/html/HTMLInputElement.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/html/HTMLInputElement.cpp')
-rw-r--r--dom/html/HTMLInputElement.cpp7407
1 files changed, 7407 insertions, 0 deletions
diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp
new file mode 100644
index 0000000000..4e7241ec7a
--- /dev/null
+++ b/dom/html/HTMLInputElement.cpp
@@ -0,0 +1,7407 @@
+/* -*- 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/dom/HTMLInputElement.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/AutocompleteInfoBinding.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/DocumentOrShadowRoot.h"
+#include "mozilla/dom/ElementBinding.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/FormData.h"
+#include "mozilla/dom/GetFilesHelper.h"
+#include "mozilla/dom/NumericInputTypes.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/InputType.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/dom/WheelEventBinding.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_signon.h"
+#include "mozilla/TextUtils.h"
+#include "mozilla/Try.h"
+#include "nsAttrValueInlines.h"
+#include "nsCRTGlue.h"
+#include "nsIFilePicker.h"
+#include "nsNetUtil.h"
+#include "nsQueryObject.h"
+
+#include "HTMLDataListElement.h"
+#include "HTMLFormSubmissionConstants.h"
+#include "mozilla/Telemetry.h"
+#include "nsBaseCommandController.h"
+#include "nsIStringBundle.h"
+#include "nsFocusManager.h"
+#include "nsColorControlFrame.h"
+#include "nsNumberControlFrame.h"
+#include "nsSearchControlFrame.h"
+#include "nsPIDOMWindow.h"
+#include "nsRepeatService.h"
+#include "nsContentCID.h"
+#include "mozilla/dom/ProgressEvent.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsPresContext.h"
+#include "nsIFormControl.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLDataListElement.h"
+#include "mozilla/dom/HTMLOptionElement.h"
+#include "nsIFormControlFrame.h"
+#include "nsITextControlFrame.h"
+#include "nsIFrame.h"
+#include "nsRangeFrame.h"
+#include "nsError.h"
+#include "nsIEditor.h"
+#include "nsIPromptCollection.h"
+
+#include "mozilla/PresState.h"
+#include "nsLinebreakConverter.h" //to strip out carriage returns
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsVariant.h"
+
+#include "mozilla/ContentEvents.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/TextControlState.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TouchEvents.h"
+
+#include <algorithm>
+
+// input type=radio
+#include "mozilla/dom/RadioGroupContainer.h"
+#include "nsIRadioVisitor.h"
+#include "nsRadioVisitor.h"
+
+// input type=file
+#include "mozilla/dom/FileSystemEntry.h"
+#include "mozilla/dom/FileSystem.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileList.h"
+#include "nsIFile.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIContentPrefService2.h"
+#include "nsIMIMEService.h"
+#include "nsIObserverService.h"
+
+// input type=image
+#include "nsImageLoadingContent.h"
+#include "imgRequestProxy.h"
+
+#include "mozAutoDocUpdate.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/DirectionalityUtils.h"
+
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/MathAlgorithms.h"
+
+#include <limits>
+
+#include "nsIColorPicker.h"
+#include "nsIStringEnumerator.h"
+#include "HTMLSplitOnSpacesTokenizer.h"
+#include "nsIMIMEInfo.h"
+#include "nsFrameSelection.h"
+#include "nsXULControllers.h"
+
+// input type=date
+#include "js/Date.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Input)
+
+// XXX align=left, hspace, vspace, border? other nav4 attrs
+
+namespace mozilla::dom {
+
+// First bits are needed for the control type.
+#define NS_OUTER_ACTIVATE_EVENT (1 << 9)
+#define NS_ORIGINAL_CHECKED_VALUE (1 << 10)
+// (1 << 11 is unused)
+#define NS_ORIGINAL_INDETERMINATE_VALUE (1 << 12)
+#define NS_PRE_HANDLE_BLUR_EVENT (1 << 13)
+#define NS_IN_SUBMIT_CLICK (1 << 15)
+#define NS_CONTROL_TYPE(bits) \
+ ((bits) & ~(NS_OUTER_ACTIVATE_EVENT | NS_ORIGINAL_CHECKED_VALUE | \
+ NS_ORIGINAL_INDETERMINATE_VALUE | NS_PRE_HANDLE_BLUR_EVENT | \
+ NS_IN_SUBMIT_CLICK))
+
+// whether textfields should be selected once focused:
+// -1: no, 1: yes, 0: uninitialized
+static int32_t gSelectTextFieldOnFocus;
+UploadLastDir* HTMLInputElement::gUploadLastDir;
+
+static const nsAttrValue::EnumTable kInputTypeTable[] = {
+ {"button", FormControlType::InputButton},
+ {"checkbox", FormControlType::InputCheckbox},
+ {"color", FormControlType::InputColor},
+ {"date", FormControlType::InputDate},
+ {"datetime-local", FormControlType::InputDatetimeLocal},
+ {"email", FormControlType::InputEmail},
+ {"file", FormControlType::InputFile},
+ {"hidden", FormControlType::InputHidden},
+ {"reset", FormControlType::InputReset},
+ {"image", FormControlType::InputImage},
+ {"month", FormControlType::InputMonth},
+ {"number", FormControlType::InputNumber},
+ {"password", FormControlType::InputPassword},
+ {"radio", FormControlType::InputRadio},
+ {"range", FormControlType::InputRange},
+ {"search", FormControlType::InputSearch},
+ {"submit", FormControlType::InputSubmit},
+ {"tel", FormControlType::InputTel},
+ {"time", FormControlType::InputTime},
+ {"url", FormControlType::InputUrl},
+ {"week", FormControlType::InputWeek},
+ // "text" must be last for ParseAttribute to work right. If you add things
+ // before it, please update kInputDefaultType.
+ {"text", FormControlType::InputText},
+ {nullptr, 0}};
+
+// Default type is 'text'.
+static const nsAttrValue::EnumTable* kInputDefaultType =
+ &kInputTypeTable[ArrayLength(kInputTypeTable) - 2];
+
+static const nsAttrValue::EnumTable kCaptureTable[] = {
+ {"user", nsIFilePicker::captureUser},
+ {"environment", nsIFilePicker::captureEnv},
+ {"", nsIFilePicker::captureDefault},
+ {nullptr, nsIFilePicker::captureNone}};
+
+static const nsAttrValue::EnumTable* kCaptureDefault = &kCaptureTable[2];
+
+using namespace blink;
+
+constexpr Decimal HTMLInputElement::kStepScaleFactorDate(86400000_d);
+constexpr Decimal HTMLInputElement::kStepScaleFactorNumberRange(1_d);
+constexpr Decimal HTMLInputElement::kStepScaleFactorTime(1000_d);
+constexpr Decimal HTMLInputElement::kStepScaleFactorMonth(1_d);
+constexpr Decimal HTMLInputElement::kStepScaleFactorWeek(7 * 86400000_d);
+constexpr Decimal HTMLInputElement::kDefaultStepBase(0_d);
+constexpr Decimal HTMLInputElement::kDefaultStepBaseWeek(-259200000_d);
+constexpr Decimal HTMLInputElement::kDefaultStep(1_d);
+constexpr Decimal HTMLInputElement::kDefaultStepTime(60_d);
+constexpr Decimal HTMLInputElement::kStepAny(0_d);
+
+const double HTMLInputElement::kMinimumYear = 1;
+const double HTMLInputElement::kMaximumYear = 275760;
+const double HTMLInputElement::kMaximumWeekInMaximumYear = 37;
+const double HTMLInputElement::kMaximumDayInMaximumYear = 13;
+const double HTMLInputElement::kMaximumMonthInMaximumYear = 9;
+const double HTMLInputElement::kMaximumWeekInYear = 53;
+const double HTMLInputElement::kMsPerDay = 24 * 60 * 60 * 1000;
+
+// An helper class for the dispatching of the 'change' event.
+// This class is used when the FilePicker finished its task (or when files and
+// directories are set by some chrome/test only method).
+// The task of this class is to postpone the dispatching of 'change' and 'input'
+// events at the end of the exploration of the directories.
+class DispatchChangeEventCallback final : public GetFilesCallback {
+ public:
+ explicit DispatchChangeEventCallback(HTMLInputElement* aInputElement)
+ : mInputElement(aInputElement) {
+ MOZ_ASSERT(aInputElement);
+ }
+
+ virtual void Callback(
+ nsresult aStatus,
+ const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) override {
+ if (!mInputElement->GetOwnerGlobal()) {
+ return;
+ }
+
+ nsTArray<OwningFileOrDirectory> array;
+ for (uint32_t i = 0; i < aBlobImpls.Length(); ++i) {
+ OwningFileOrDirectory* element = array.AppendElement();
+ RefPtr<File> file =
+ File::Create(mInputElement->GetOwnerGlobal(), aBlobImpls[i]);
+ if (NS_WARN_IF(!file)) {
+ return;
+ }
+
+ element->SetAsFile() = file;
+ }
+
+ mInputElement->SetFilesOrDirectories(array, true);
+ Unused << NS_WARN_IF(NS_FAILED(DispatchEvents()));
+ }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult DispatchEvents() {
+ RefPtr<HTMLInputElement> inputElement(mInputElement);
+ nsresult rv = nsContentUtils::DispatchInputEvent(inputElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch input event");
+ mInputElement->SetUserInteracted(true);
+ rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
+ mInputElement, u"change"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+
+ return rv;
+ }
+
+ private:
+ RefPtr<HTMLInputElement> mInputElement;
+};
+
+struct HTMLInputElement::FileData {
+ /**
+ * The value of the input if it is a file input. This is the list of files or
+ * directories DOM objects used when uploading a file. It is vital that this
+ * is kept separate from mValue so that it won't be possible to 'leak' the
+ * value from a text-input to a file-input. Additionally, the logic for this
+ * value is kept as simple as possible to avoid accidental errors where the
+ * wrong filename is used. Therefor the list of filenames is always owned by
+ * this member, never by the frame. Whenever the frame wants to change the
+ * filename it has to call SetFilesOrDirectories to update this member.
+ */
+ nsTArray<OwningFileOrDirectory> mFilesOrDirectories;
+
+ RefPtr<GetFilesHelper> mGetFilesRecursiveHelper;
+ RefPtr<GetFilesHelper> mGetFilesNonRecursiveHelper;
+
+ /**
+ * Hack for bug 1086684: Stash the .value when we're a file picker.
+ */
+ nsString mFirstFilePath;
+
+ RefPtr<FileList> mFileList;
+ Sequence<RefPtr<FileSystemEntry>> mEntries;
+
+ nsString mStaticDocFileList;
+
+ void ClearGetFilesHelpers() {
+ if (mGetFilesRecursiveHelper) {
+ mGetFilesRecursiveHelper->Unlink();
+ mGetFilesRecursiveHelper = nullptr;
+ }
+
+ if (mGetFilesNonRecursiveHelper) {
+ mGetFilesNonRecursiveHelper->Unlink();
+ mGetFilesNonRecursiveHelper = nullptr;
+ }
+ }
+
+ // Cycle Collection support.
+ void Traverse(nsCycleCollectionTraversalCallback& cb) {
+ FileData* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFilesOrDirectories)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFileList)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEntries)
+ if (mGetFilesRecursiveHelper) {
+ mGetFilesRecursiveHelper->Traverse(cb);
+ }
+
+ if (mGetFilesNonRecursiveHelper) {
+ mGetFilesNonRecursiveHelper->Traverse(cb);
+ }
+ }
+
+ void Unlink() {
+ FileData* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mFilesOrDirectories)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mFileList)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEntries)
+ ClearGetFilesHelpers();
+ }
+};
+
+HTMLInputElement::nsFilePickerShownCallback::nsFilePickerShownCallback(
+ HTMLInputElement* aInput, nsIFilePicker* aFilePicker)
+ : mFilePicker(aFilePicker), mInput(aInput) {}
+
+NS_IMPL_ISUPPORTS(UploadLastDir::ContentPrefCallback, nsIContentPrefCallback2)
+
+NS_IMETHODIMP
+UploadLastDir::ContentPrefCallback::HandleCompletion(uint16_t aReason) {
+ nsCOMPtr<nsIFile> localFile;
+ nsAutoString prefStr;
+
+ if (aReason == nsIContentPrefCallback2::COMPLETE_ERROR || !mResult) {
+ Preferences::GetString("dom.input.fallbackUploadDir", prefStr);
+ }
+
+ if (prefStr.IsEmpty() && mResult) {
+ nsCOMPtr<nsIVariant> pref;
+ mResult->GetValue(getter_AddRefs(pref));
+ pref->GetAsAString(prefStr);
+ }
+
+ if (!prefStr.IsEmpty()) {
+ localFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
+ if (localFile && NS_WARN_IF(NS_FAILED(localFile->InitWithPath(prefStr)))) {
+ localFile = nullptr;
+ }
+ }
+
+ if (localFile) {
+ mFilePicker->SetDisplayDirectory(localFile);
+ } else {
+ // If no custom directory was set through the pref, default to
+ // "desktop" directory for each platform.
+ mFilePicker->SetDisplaySpecialDirectory(
+ NS_LITERAL_STRING_FROM_CSTRING(NS_OS_DESKTOP_DIR));
+ }
+
+ mFilePicker->Open(mFpCallback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UploadLastDir::ContentPrefCallback::HandleResult(nsIContentPref* pref) {
+ mResult = pref;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UploadLastDir::ContentPrefCallback::HandleError(nsresult error) {
+ // HandleCompletion is always called (even with HandleError was called),
+ // so we don't need to do anything special here.
+ return NS_OK;
+}
+
+namespace {
+
+/**
+ * This may return nullptr if the DOM File's implementation of
+ * File::mozFullPathInternal does not successfully return a non-empty
+ * string that is a valid path. This can happen on Firefox OS, for example,
+ * where the file picker can create Blobs.
+ */
+static already_AddRefed<nsIFile> LastUsedDirectory(
+ const OwningFileOrDirectory& aData) {
+ if (aData.IsFile()) {
+ nsAutoString path;
+ ErrorResult error;
+ aData.GetAsFile()->GetMozFullPathInternal(path, error);
+ if (error.Failed() || path.IsEmpty()) {
+ error.SuppressException();
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIFile> localFile;
+ nsresult rv = NS_NewLocalFile(path, true, getter_AddRefs(localFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIFile> parentFile;
+ rv = localFile->GetParent(getter_AddRefs(parentFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return parentFile.forget();
+ }
+
+ MOZ_ASSERT(aData.IsDirectory());
+
+ nsCOMPtr<nsIFile> localFile = aData.GetAsDirectory()->GetInternalNsIFile();
+ MOZ_ASSERT(localFile);
+
+ return localFile.forget();
+}
+
+void GetDOMFileOrDirectoryName(const OwningFileOrDirectory& aData,
+ nsAString& aName) {
+ if (aData.IsFile()) {
+ aData.GetAsFile()->GetName(aName);
+ } else {
+ MOZ_ASSERT(aData.IsDirectory());
+ ErrorResult rv;
+ aData.GetAsDirectory()->GetName(aName, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ }
+ }
+}
+
+void GetDOMFileOrDirectoryPath(const OwningFileOrDirectory& aData,
+ nsAString& aPath, ErrorResult& aRv) {
+ if (aData.IsFile()) {
+ aData.GetAsFile()->GetMozFullPathInternal(aPath, aRv);
+ } else {
+ MOZ_ASSERT(aData.IsDirectory());
+ aData.GetAsDirectory()->GetFullRealPath(aPath);
+ }
+}
+
+} // namespace
+
+NS_IMETHODIMP
+HTMLInputElement::nsFilePickerShownCallback::Done(
+ nsIFilePicker::ResultCode aResult) {
+ mInput->PickerClosed();
+
+ if (aResult == nsIFilePicker::returnCancel) {
+ RefPtr<HTMLInputElement> inputElement(mInput);
+ return nsContentUtils::DispatchTrustedEvent(
+ inputElement->OwnerDoc(), inputElement, u"cancel"_ns, CanBubble::eYes,
+ Cancelable::eNo);
+ }
+
+ mInput->OwnerDoc()->NotifyUserGestureActivation();
+
+ nsIFilePicker::Mode mode;
+ mFilePicker->GetMode(&mode);
+
+ // Collect new selected filenames
+ nsTArray<OwningFileOrDirectory> newFilesOrDirectories;
+ if (mode == nsIFilePicker::modeOpenMultiple) {
+ nsCOMPtr<nsISimpleEnumerator> iter;
+ nsresult rv =
+ mFilePicker->GetDomFileOrDirectoryEnumerator(getter_AddRefs(iter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!iter) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISupports> tmp;
+ bool hasMore = true;
+
+ while (NS_SUCCEEDED(iter->HasMoreElements(&hasMore)) && hasMore) {
+ iter->GetNext(getter_AddRefs(tmp));
+ RefPtr<Blob> domBlob = do_QueryObject(tmp);
+ MOZ_ASSERT(domBlob,
+ "Null file object from FilePicker's file enumerator?");
+ if (!domBlob) {
+ continue;
+ }
+
+ OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
+ element->SetAsFile() = domBlob->ToFile();
+ }
+ } else {
+ MOZ_ASSERT(mode == nsIFilePicker::modeOpen ||
+ mode == nsIFilePicker::modeGetFolder);
+ nsCOMPtr<nsISupports> tmp;
+ nsresult rv = mFilePicker->GetDomFileOrDirectory(getter_AddRefs(tmp));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Show a prompt to get user confirmation before allowing folder access.
+ // This is to prevent sites from tricking the user into uploading files.
+ // See Bug 1338637.
+ if (mode == nsIFilePicker::modeGetFolder) {
+ nsCOMPtr<nsIPromptCollection> prompter =
+ do_GetService("@mozilla.org/embedcomp/prompt-collection;1");
+ if (!prompter) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ bool confirmed = false;
+ BrowsingContext* bc = mInput->OwnerDoc()->GetBrowsingContext();
+
+ // Get directory name
+ RefPtr<Directory> directory = static_cast<Directory*>(tmp.get());
+ nsAutoString directoryName;
+ ErrorResult error;
+ directory->GetName(directoryName, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+
+ rv = prompter->ConfirmFolderUpload(bc, directoryName, &confirmed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!confirmed) {
+ // User aborted upload
+ return NS_OK;
+ }
+ }
+
+ RefPtr<Blob> blob = do_QueryObject(tmp);
+ if (blob) {
+ RefPtr<File> file = blob->ToFile();
+ MOZ_ASSERT(file);
+
+ OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
+ element->SetAsFile() = file;
+ } else if (tmp) {
+ RefPtr<Directory> directory = static_cast<Directory*>(tmp.get());
+ OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
+ element->SetAsDirectory() = directory;
+ }
+ }
+
+ if (newFilesOrDirectories.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // Store the last used directory using the content pref service:
+ nsCOMPtr<nsIFile> lastUsedDir = LastUsedDirectory(newFilesOrDirectories[0]);
+
+ if (lastUsedDir) {
+ HTMLInputElement::gUploadLastDir->StoreLastUsedDirectory(mInput->OwnerDoc(),
+ lastUsedDir);
+ }
+
+ // The text control frame (if there is one) isn't going to send a change
+ // event because it will think this is done by a script.
+ // So, we can safely send one by ourself.
+ mInput->SetFilesOrDirectories(newFilesOrDirectories, true);
+
+ // mInput(HTMLInputElement) has no scriptGlobalObject, don't create
+ // DispatchChangeEventCallback
+ if (!mInput->GetOwnerGlobal()) {
+ return NS_OK;
+ }
+ RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
+ new DispatchChangeEventCallback(mInput);
+
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ mInput->HasAttr(nsGkAtoms::webkitdirectory)) {
+ ErrorResult error;
+ GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+
+ helper->AddCallback(dispatchChangeEventCallback);
+ return NS_OK;
+ }
+
+ return dispatchChangeEventCallback->DispatchEvents();
+}
+
+NS_IMPL_ISUPPORTS(HTMLInputElement::nsFilePickerShownCallback,
+ nsIFilePickerShownCallback)
+
+class nsColorPickerShownCallback final : public nsIColorPickerShownCallback {
+ ~nsColorPickerShownCallback() = default;
+
+ public:
+ nsColorPickerShownCallback(HTMLInputElement* aInput,
+ nsIColorPicker* aColorPicker)
+ : mInput(aInput), mColorPicker(aColorPicker), mValueChanged(false) {}
+
+ NS_DECL_ISUPPORTS
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD Update(const nsAString& aColor) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD Done(const nsAString& aColor) override;
+
+ private:
+ /**
+ * Updates the internals of the object using aColor as the new value.
+ * If aTrustedUpdate is true, it will consider that aColor is a new value.
+ * Otherwise, it will check that aColor is different from the current value.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult UpdateInternal(const nsAString& aColor, bool aTrustedUpdate);
+
+ RefPtr<HTMLInputElement> mInput;
+ nsCOMPtr<nsIColorPicker> mColorPicker;
+ bool mValueChanged;
+};
+
+nsresult nsColorPickerShownCallback::UpdateInternal(const nsAString& aColor,
+ bool aTrustedUpdate) {
+ bool valueChanged = false;
+ nsAutoString oldValue;
+ if (aTrustedUpdate) {
+ mInput->OwnerDoc()->NotifyUserGestureActivation();
+ valueChanged = true;
+ } else {
+ mInput->GetValue(oldValue, CallerType::System);
+ }
+
+ mInput->SetValue(aColor, CallerType::System, IgnoreErrors());
+
+ if (!aTrustedUpdate) {
+ nsAutoString newValue;
+ mInput->GetValue(newValue, CallerType::System);
+ if (!oldValue.Equals(newValue)) {
+ valueChanged = true;
+ }
+ }
+
+ if (!valueChanged) {
+ return NS_OK;
+ }
+
+ mValueChanged = true;
+ RefPtr<HTMLInputElement> input(mInput);
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(input);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsColorPickerShownCallback::Update(const nsAString& aColor) {
+ return UpdateInternal(aColor, true);
+}
+
+NS_IMETHODIMP
+nsColorPickerShownCallback::Done(const nsAString& aColor) {
+ /**
+ * When Done() is called, we might be at the end of a serie of Update() calls
+ * in which case mValueChanged is set to true and a change event will have to
+ * be fired but we might also be in a one shot Done() call situation in which
+ * case we should fire a change event iif the value actually changed.
+ * UpdateInternal(bool) is taking care of that logic for us.
+ */
+ nsresult rv = NS_OK;
+
+ mInput->PickerClosed();
+
+ if (!aColor.IsEmpty()) {
+ UpdateInternal(aColor, false);
+ }
+
+ if (mValueChanged) {
+ mInput->SetUserInteracted(true);
+ rv = nsContentUtils::DispatchTrustedEvent(
+ mInput->OwnerDoc(), static_cast<Element*>(mInput.get()), u"change"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+ }
+
+ return rv;
+}
+
+NS_IMPL_ISUPPORTS(nsColorPickerShownCallback, nsIColorPickerShownCallback)
+
+static bool IsPopupBlocked(Document* aDoc) {
+ if (aDoc->ConsumeTransientUserGestureActivation()) {
+ return false;
+ }
+
+ WindowContext* wc = aDoc->GetWindowContext();
+ if (wc && wc->CanShowPopup()) {
+ return false;
+ }
+
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, aDoc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "InputPickerBlockedNoUserActivation");
+ return true;
+}
+
+nsTArray<nsString> HTMLInputElement::GetColorsFromList() {
+ RefPtr<HTMLDataListElement> dataList = GetList();
+ if (!dataList) {
+ return {};
+ }
+
+ nsTArray<nsString> colors;
+
+ RefPtr<nsContentList> options = dataList->Options();
+ uint32_t length = options->Length(true);
+ for (uint32_t i = 0; i < length; ++i) {
+ auto* option = HTMLOptionElement::FromNodeOrNull(options->Item(i, false));
+ if (!option) {
+ continue;
+ }
+
+ nsString value;
+ option->GetValue(value);
+ if (IsValidSimpleColor(value)) {
+ ToLowerCase(value);
+ colors.AppendElement(value);
+ }
+ }
+
+ return colors;
+}
+
+nsresult HTMLInputElement::InitColorPicker() {
+ MOZ_ASSERT(IsMutable());
+
+ if (mPickerRunning) {
+ NS_WARNING("Just one nsIColorPicker is allowed");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<Document> doc = OwnerDoc();
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow();
+ if (!win) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (IsPopupBlocked(doc)) {
+ return NS_OK;
+ }
+
+ // Get Loc title
+ nsAutoString title;
+ nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "ColorPicker", title);
+
+ nsCOMPtr<nsIColorPicker> colorPicker =
+ do_CreateInstance("@mozilla.org/colorpicker;1");
+ if (!colorPicker) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoString initialValue;
+ GetNonFileValueInternal(initialValue);
+ nsTArray<nsString> colors = GetColorsFromList();
+ nsresult rv = colorPicker->Init(win, title, initialValue, colors);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIColorPickerShownCallback> callback =
+ new nsColorPickerShownCallback(this, colorPicker);
+
+ rv = colorPicker->Open(callback);
+ if (NS_SUCCEEDED(rv)) {
+ mPickerRunning = true;
+ }
+
+ return rv;
+}
+
+nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) {
+ MOZ_ASSERT(IsMutable());
+
+ if (mPickerRunning) {
+ NS_WARNING("Just one nsIFilePicker is allowed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get parent nsPIDOMWindow object.
+ nsCOMPtr<Document> doc = OwnerDoc();
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow();
+ if (!win) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (IsPopupBlocked(doc)) {
+ return NS_OK;
+ }
+
+ // Get Loc title
+ nsAutoString title;
+ nsAutoString okButtonLabel;
+ if (aType == FILE_PICKER_DIRECTORY) {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "DirectoryUpload", OwnerDoc(),
+ title);
+
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "DirectoryPickerOkButtonLabel",
+ OwnerDoc(), okButtonLabel);
+ } else {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "FileUpload", OwnerDoc(), title);
+ }
+
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1");
+ if (!filePicker) return NS_ERROR_FAILURE;
+
+ nsIFilePicker::Mode mode;
+
+ if (aType == FILE_PICKER_DIRECTORY) {
+ mode = nsIFilePicker::modeGetFolder;
+ } else if (HasAttr(nsGkAtoms::multiple)) {
+ mode = nsIFilePicker::modeOpenMultiple;
+ } else {
+ mode = nsIFilePicker::modeOpen;
+ }
+
+ nsresult rv =
+ filePicker->Init(win, title, mode, OwnerDoc()->GetBrowsingContext());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!okButtonLabel.IsEmpty()) {
+ filePicker->SetOkButtonLabel(okButtonLabel);
+ }
+
+ // Native directory pickers ignore file type filters, so we don't spend
+ // cycles adding them for FILE_PICKER_DIRECTORY.
+ if (HasAttr(nsGkAtoms::accept) && aType != FILE_PICKER_DIRECTORY) {
+ SetFilePickerFiltersFromAccept(filePicker);
+
+ if (StaticPrefs::dom_capture_enabled()) {
+ if (const nsAttrValue* captureVal = GetParsedAttr(nsGkAtoms::capture)) {
+ filePicker->SetCapture(static_cast<nsIFilePicker::CaptureTarget>(
+ captureVal->GetEnumValue()));
+ }
+ }
+ } else {
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+ }
+
+ // Set default directory and filename
+ nsAutoString defaultName;
+
+ const nsTArray<OwningFileOrDirectory>& oldFiles =
+ GetFilesOrDirectoriesInternal();
+
+ nsCOMPtr<nsIFilePickerShownCallback> callback =
+ new HTMLInputElement::nsFilePickerShownCallback(this, filePicker);
+
+ if (!oldFiles.IsEmpty() && aType != FILE_PICKER_DIRECTORY) {
+ nsAutoString path;
+
+ nsCOMPtr<nsIFile> parentFile = LastUsedDirectory(oldFiles[0]);
+ if (parentFile) {
+ filePicker->SetDisplayDirectory(parentFile);
+ }
+
+ // Unfortunately nsIFilePicker doesn't allow multiple files to be
+ // default-selected, so only select something by default if exactly
+ // one file was selected before.
+ if (oldFiles.Length() == 1) {
+ nsAutoString leafName;
+ GetDOMFileOrDirectoryName(oldFiles[0], leafName);
+
+ if (!leafName.IsEmpty()) {
+ filePicker->SetDefaultString(leafName);
+ }
+ }
+
+ rv = filePicker->Open(callback);
+ if (NS_SUCCEEDED(rv)) {
+ mPickerRunning = true;
+ }
+
+ return rv;
+ }
+
+ HTMLInputElement::gUploadLastDir->FetchDirectoryAndDisplayPicker(
+ doc, filePicker, callback);
+ mPickerRunning = true;
+ return NS_OK;
+}
+
+#define CPS_PREF_NAME u"browser.upload.lastDir"_ns
+
+NS_IMPL_ISUPPORTS(UploadLastDir, nsIObserver, nsISupportsWeakReference)
+
+void HTMLInputElement::InitUploadLastDir() {
+ gUploadLastDir = new UploadLastDir();
+ NS_ADDREF(gUploadLastDir);
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService && gUploadLastDir) {
+ observerService->AddObserver(gUploadLastDir,
+ "browser:purge-session-history", true);
+ }
+}
+
+void HTMLInputElement::DestroyUploadLastDir() { NS_IF_RELEASE(gUploadLastDir); }
+
+nsresult UploadLastDir::FetchDirectoryAndDisplayPicker(
+ Document* aDoc, nsIFilePicker* aFilePicker,
+ nsIFilePickerShownCallback* aFpCallback) {
+ MOZ_ASSERT(aDoc, "aDoc is null");
+ MOZ_ASSERT(aFilePicker, "aFilePicker is null");
+ MOZ_ASSERT(aFpCallback, "aFpCallback is null");
+
+ nsIURI* docURI = aDoc->GetDocumentURI();
+ MOZ_ASSERT(docURI, "docURI is null");
+
+ nsCOMPtr<nsILoadContext> loadContext = aDoc->GetLoadContext();
+ nsCOMPtr<nsIContentPrefCallback2> prefCallback =
+ new UploadLastDir::ContentPrefCallback(aFilePicker, aFpCallback);
+
+ // Attempt to get the CPS, if it's not present we'll fallback to use the
+ // Desktop folder
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ if (!contentPrefService) {
+ prefCallback->HandleCompletion(nsIContentPrefCallback2::COMPLETE_ERROR);
+ return NS_OK;
+ }
+
+ nsAutoCString cstrSpec;
+ docURI->GetSpec(cstrSpec);
+ NS_ConvertUTF8toUTF16 spec(cstrSpec);
+
+ contentPrefService->GetByDomainAndName(spec, CPS_PREF_NAME, loadContext,
+ prefCallback);
+ return NS_OK;
+}
+
+nsresult UploadLastDir::StoreLastUsedDirectory(Document* aDoc, nsIFile* aDir) {
+ MOZ_ASSERT(aDoc, "aDoc is null");
+ if (!aDir) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> docURI = aDoc->GetDocumentURI();
+ MOZ_ASSERT(docURI, "docURI is null");
+
+ // Attempt to get the CPS, if it's not present we'll just return
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ if (!contentPrefService) return NS_ERROR_NOT_AVAILABLE;
+
+ nsAutoCString cstrSpec;
+ docURI->GetSpec(cstrSpec);
+ NS_ConvertUTF8toUTF16 spec(cstrSpec);
+
+ // Find the parent of aFile, and store it
+ nsString unicodePath;
+ aDir->GetPath(unicodePath);
+ if (unicodePath.IsEmpty()) // nothing to do
+ return NS_OK;
+ RefPtr<nsVariantCC> prefValue = new nsVariantCC();
+ prefValue->SetAsAString(unicodePath);
+
+ // Use the document's current load context to ensure that the content pref
+ // service doesn't persistently store this directory for this domain if the
+ // user is using private browsing:
+ nsCOMPtr<nsILoadContext> loadContext = aDoc->GetLoadContext();
+ return contentPrefService->Set(spec, CPS_PREF_NAME, prefValue, loadContext,
+ nullptr);
+}
+
+NS_IMETHODIMP
+UploadLastDir::Observe(nsISupports* aSubject, char const* aTopic,
+ char16_t const* aData) {
+ if (strcmp(aTopic, "browser:purge-session-history") == 0) {
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ if (contentPrefService)
+ contentPrefService->RemoveByName(CPS_PREF_NAME, nullptr, nullptr);
+ }
+ return NS_OK;
+}
+
+#ifdef ACCESSIBILITY
+// Helper method
+static nsresult FireEventForAccessibility(HTMLInputElement* aTarget,
+ EventMessage aEventMessage);
+#endif
+
+//
+// construction, destruction
+//
+
+HTMLInputElement::HTMLInputElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser, FromClone aFromClone)
+ : TextControlElement(std::move(aNodeInfo), aFromParser,
+ FormControlType(kInputDefaultType->value)),
+ mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown),
+ mAutocompleteInfoState(nsContentUtils::eAutocompleteAttrState_Unknown),
+ mDisabledChanged(false),
+ mValueChanged(false),
+ mUserInteracted(false),
+ mLastValueChangeWasInteractive(false),
+ mCheckedChanged(false),
+ mChecked(false),
+ mHandlingSelectEvent(false),
+ mShouldInitChecked(false),
+ mDoneCreating(aFromParser == NOT_FROM_PARSER &&
+ aFromClone == FromClone::No),
+ mInInternalActivate(false),
+ mCheckedIsToggled(false),
+ mIndeterminate(false),
+ mInhibitRestoration(aFromParser & FROM_PARSER_FRAGMENT),
+ mHasRange(false),
+ mIsDraggingRange(false),
+ mNumberControlSpinnerIsSpinning(false),
+ mNumberControlSpinnerSpinsUp(false),
+ mPickerRunning(false),
+ mIsPreviewEnabled(false),
+ mHasBeenTypePassword(false),
+ mHasPatternAttribute(false),
+ mRadioGroupContainer(nullptr) {
+ // If size is above 512, mozjemalloc allocates 1kB, see
+ // memory/build/mozjemalloc.cpp
+ static_assert(sizeof(HTMLInputElement) <= 512,
+ "Keep the size of HTMLInputElement under 512 to avoid "
+ "performance regression!");
+
+ // We are in a type=text but we create TextControlState lazily.
+ mInputData.mState = nullptr;
+
+ void* memory = mInputTypeMem;
+ mInputType = InputType::Create(this, mType, memory);
+
+ if (!gUploadLastDir) HTMLInputElement::InitUploadLastDir();
+
+ // Set up our default state. By default we're enabled (since we're a control
+ // type that can be disabled but not actually disabled right now), optional,
+ // read-write, and valid. Also by default we don't have to show validity UI
+ // and so forth.
+ AddStatesSilently(ElementState::ENABLED | ElementState::OPTIONAL_ |
+ ElementState::VALID | ElementState::VALUE_EMPTY |
+ ElementState::READWRITE);
+ RemoveStatesSilently(ElementState::READONLY);
+ UpdateApzAwareFlag();
+}
+
+HTMLInputElement::~HTMLInputElement() {
+ if (mNumberControlSpinnerIsSpinning) {
+ StopNumberControlSpinnerSpin(eDisallowDispatchingEvents);
+ }
+ nsImageLoadingContent::Destroy();
+ FreeData();
+}
+
+void HTMLInputElement::FreeData() {
+ if (!IsSingleLineTextControl(false)) {
+ free(mInputData.mValue);
+ mInputData.mValue = nullptr;
+ } else if (mInputData.mState) {
+ // XXX Passing nullptr to UnbindFromFrame doesn't do anything!
+ UnbindFromFrame(nullptr);
+ mInputData.mState->Destroy();
+ mInputData.mState = nullptr;
+ }
+
+ if (mInputType) {
+ mInputType->DropReference();
+ mInputType = nullptr;
+ }
+}
+
+void HTMLInputElement::EnsureEditorState() {
+ MOZ_ASSERT(IsSingleLineTextControl(false));
+ if (!mInputData.mState) {
+ mInputData.mState = TextControlState::Construct(this);
+ }
+}
+
+TextControlState* HTMLInputElement::GetEditorState() const {
+ if (!IsSingleLineTextControl(false)) {
+ return nullptr;
+ }
+
+ // We've postponed allocating TextControlState, doing that in a const
+ // method is fine.
+ const_cast<HTMLInputElement*>(this)->EnsureEditorState();
+
+ MOZ_ASSERT(mInputData.mState,
+ "Single line text controls need to have a state"
+ " associated with them");
+
+ return mInputData.mState;
+}
+
+// nsISupports
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLInputElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLInputElement,
+ TextControlElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers)
+ if (tmp->IsSingleLineTextControl(false) && tmp->mInputData.mState) {
+ tmp->mInputData.mState->Traverse(cb);
+ }
+
+ if (tmp->mFileData) {
+ tmp->mFileData->Traverse(cb);
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLInputElement,
+ TextControlElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers)
+ if (tmp->IsSingleLineTextControl(false) && tmp->mInputData.mState) {
+ tmp->mInputData.mState->Unlink();
+ }
+
+ if (tmp->mFileData) {
+ tmp->mFileData->Unlink();
+ }
+ // XXX should unlink more?
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLInputElement,
+ TextControlElement,
+ imgINotificationObserver,
+ nsIImageLoadingContent,
+ nsIConstraintValidation)
+
+// nsINode
+
+nsresult HTMLInputElement::Clone(dom::NodeInfo* aNodeInfo,
+ nsINode** aResult) const {
+ *aResult = nullptr;
+
+ RefPtr<HTMLInputElement> it = new (aNodeInfo->NodeInfoManager())
+ HTMLInputElement(do_AddRef(aNodeInfo), NOT_FROM_PARSER, FromClone::Yes);
+
+ nsresult rv = const_cast<HTMLInputElement*>(this)->CopyInnerTo(it);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch (GetValueMode()) {
+ case VALUE_MODE_VALUE:
+ if (mValueChanged) {
+ // We don't have our default value anymore. Set our value on
+ // the clone.
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ // SetValueInternal handles setting the VALUE_CHANGED bit for us
+ if (NS_WARN_IF(
+ NS_FAILED(rv = it->SetValueInternal(
+ value, {ValueSetterOption::SetValueChanged})))) {
+ return rv;
+ }
+ }
+ break;
+ case VALUE_MODE_FILENAME:
+ if (it->OwnerDoc()->IsStaticDocument()) {
+ // We're going to be used in print preview. Since the doc is static
+ // we can just grab the pretty string and use it as wallpaper
+ GetDisplayFileName(it->mFileData->mStaticDocFileList);
+ } else {
+ it->mFileData->ClearGetFilesHelpers();
+ it->mFileData->mFilesOrDirectories.Clear();
+ it->mFileData->mFilesOrDirectories.AppendElements(
+ mFileData->mFilesOrDirectories);
+ }
+ break;
+ case VALUE_MODE_DEFAULT_ON:
+ case VALUE_MODE_DEFAULT:
+ break;
+ }
+
+ if (mCheckedChanged) {
+ // We no longer have our original checked state. Set our
+ // checked state on the clone.
+ it->DoSetChecked(mChecked, false, true);
+ // Then tell DoneCreatingElement() not to overwrite:
+ it->mShouldInitChecked = false;
+ }
+
+ it->mIndeterminate = mIndeterminate;
+
+ it->DoneCreatingElement();
+
+ it->SetLastValueChangeWasInteractive(mLastValueChangeWasInteractive);
+ it.forget(aResult);
+ return NS_OK;
+}
+
+void HTMLInputElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aNotify && aName == nsGkAtoms::disabled) {
+ mDisabledChanged = true;
+ }
+
+ // When name or type changes, radio should be removed from radio group.
+ // If we are not done creating the radio, we also should not do it.
+ if (mType == FormControlType::InputRadio) {
+ if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) &&
+ (mForm || mDoneCreating)) {
+ RemoveFromRadioGroup();
+ } else if (aName == nsGkAtoms::required) {
+ auto* container = GetCurrentRadioGroupContainer();
+
+ if (container && ((aValue && !HasAttr(aNameSpaceID, aName)) ||
+ (!aValue && HasAttr(aNameSpaceID, aName)))) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ container->RadioRequiredWillChange(name, !!aValue);
+ }
+ }
+ }
+
+ if (aName == nsGkAtoms::webkitdirectory) {
+ Telemetry::Accumulate(Telemetry::WEBKIT_DIRECTORY_USED, true);
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::BeforeSetAttr(
+ aNameSpaceID, aName, aValue, aNotify);
+}
+
+void HTMLInputElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ bool needValidityUpdate = false;
+ if (aName == nsGkAtoms::src) {
+ mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue ? aValue->GetStringValue() : EmptyString(),
+ aSubjectPrincipal);
+ if (aNotify && mType == FormControlType::InputImage) {
+ if (aValue) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ LoadImage(aValue->GetStringValue(), true, aNotify,
+ eImageLoadType_Normal, mSrcTriggeringPrincipal);
+ } else {
+ // Null value means the attr got unset; drop the image
+ CancelImageRequests(aNotify);
+ }
+ }
+ }
+
+ if (aName == nsGkAtoms::value) {
+ // If the element has a value in value mode, the value content attribute
+ // is the default value. So if the elements value didn't change from the
+ // default, we have to re-set it.
+ if (!mValueChanged && GetValueMode() == VALUE_MODE_VALUE) {
+ SetDefaultValueAsValue();
+ } else if (GetValueMode() == VALUE_MODE_DEFAULT && HasDirAuto()) {
+ SetAutoDirectionality(aNotify);
+ }
+ // GetStepBase() depends on the `value` attribute if `min` is not present,
+ // even if the value doesn't change.
+ UpdateStepMismatchValidityState();
+ needValidityUpdate = true;
+ }
+
+ // Checked must be set no matter what type of control it is, since
+ // mChecked must reflect the new value
+ if (aName == nsGkAtoms::checked) {
+ if (IsRadioOrCheckbox()) {
+ SetStates(ElementState::DEFAULT, !!aValue, aNotify);
+ }
+ if (!mCheckedChanged) {
+ // Delay setting checked if we are creating this element (wait
+ // until everything is set)
+ if (!mDoneCreating) {
+ mShouldInitChecked = true;
+ } else {
+ DoSetChecked(!!aValue, aNotify, false);
+ }
+ }
+ needValidityUpdate = true;
+ }
+
+ if (aName == nsGkAtoms::type) {
+ FormControlType newType;
+ if (!aValue) {
+ // We're now a text input.
+ newType = FormControlType(kInputDefaultType->value);
+ } else {
+ newType = FormControlType(aValue->GetEnumValue());
+ }
+ if (newType != mType) {
+ HandleTypeChange(newType, aNotify);
+ needValidityUpdate = true;
+ }
+ }
+
+ // When name or type changes, radio should be added to radio group.
+ // If we are not done creating the radio, we also should not do it.
+ if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) &&
+ mType == FormControlType::InputRadio && (mForm || mDoneCreating)) {
+ AddToRadioGroup();
+ UpdateValueMissingValidityStateForRadio(false);
+ needValidityUpdate = true;
+ }
+
+ if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled ||
+ aName == nsGkAtoms::readonly) {
+ if (aName == nsGkAtoms::disabled) {
+ // This *has* to be called *before* validity state check because
+ // UpdateBarredFromConstraintValidation and
+ // UpdateValueMissingValidityState depend on our disabled state.
+ UpdateDisabledState(aNotify);
+ }
+
+ if (aName == nsGkAtoms::required && DoesRequiredApply()) {
+ // This *has* to be called *before* UpdateValueMissingValidityState
+ // because UpdateValueMissingValidityState depends on our required
+ // state.
+ UpdateRequiredState(!!aValue, aNotify);
+ }
+
+ if (aName == nsGkAtoms::readonly && !!aValue != !!aOldValue) {
+ UpdateReadOnlyState(aNotify);
+ }
+
+ UpdateValueMissingValidityState();
+
+ // This *has* to be called *after* validity has changed.
+ if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) {
+ UpdateBarredFromConstraintValidation();
+ }
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::maxlength) {
+ UpdateTooLongValidityState();
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::minlength) {
+ UpdateTooShortValidityState();
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::pattern) {
+ // Although pattern attribute only applies to single line text controls,
+ // we set this flag for all input types to save having to check the type
+ // here.
+ mHasPatternAttribute = !!aValue;
+
+ if (mDoneCreating) {
+ UpdatePatternMismatchValidityState();
+ }
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::multiple) {
+ UpdateTypeMismatchValidityState();
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::max) {
+ UpdateHasRange(aNotify);
+ mInputType->MinMaxStepAttrChanged();
+ // Validity state must be updated *after* the UpdateValueDueToAttrChange
+ // call above or else the following assert will not be valid.
+ // We don't assert the state of underflow during creation since
+ // DoneCreatingElement sanitizes.
+ UpdateRangeOverflowValidityState();
+ needValidityUpdate = true;
+ MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
+ !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
+ "HTML5 spec does not allow underflow for type=range");
+ } else if (aName == nsGkAtoms::min) {
+ UpdateHasRange(aNotify);
+ mInputType->MinMaxStepAttrChanged();
+ // See corresponding @max comment
+ UpdateRangeUnderflowValidityState();
+ UpdateStepMismatchValidityState();
+ needValidityUpdate = true;
+ MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
+ !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
+ "HTML5 spec does not allow underflow for type=range");
+ } else if (aName == nsGkAtoms::step) {
+ mInputType->MinMaxStepAttrChanged();
+ // See corresponding @max comment
+ UpdateStepMismatchValidityState();
+ needValidityUpdate = true;
+ MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
+ !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
+ "HTML5 spec does not allow underflow for type=range");
+ } else if (aName == nsGkAtoms::dir && aValue &&
+ aValue->Equals(nsGkAtoms::_auto, eIgnoreCase)) {
+ SetAutoDirectionality(aNotify);
+ } else if (aName == nsGkAtoms::lang) {
+ // FIXME(emilio, bug 1651070): This doesn't account for lang changes on
+ // ancestors.
+ if (mType == FormControlType::InputNumber) {
+ // The validity of our value may have changed based on the locale.
+ UpdateValidityState();
+ needValidityUpdate = true;
+ }
+ } else if (aName == nsGkAtoms::autocomplete) {
+ // Clear the cached @autocomplete attribute and autocompleteInfo state.
+ mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown;
+ mAutocompleteInfoState = nsContentUtils::eAutocompleteAttrState_Unknown;
+ } else if (aName == nsGkAtoms::placeholder) {
+ // Full addition / removals of the attribute reconstruct right now.
+ if (nsTextControlFrame* f = do_QueryFrame(GetPrimaryFrame())) {
+ f->PlaceholderChanged(aOldValue, aValue);
+ }
+ UpdatePlaceholderShownState();
+ needValidityUpdate = true;
+ }
+
+ if (CreatesDateTimeWidget()) {
+ if (aName == nsGkAtoms::value || aName == nsGkAtoms::readonly ||
+ aName == nsGkAtoms::tabindex || aName == nsGkAtoms::required ||
+ aName == nsGkAtoms::disabled) {
+ // If original target is this and not the inner text control, we should
+ // pass the focus to the inner text control.
+ if (Element* dateTimeBoxElement = GetDateTimeBoxElement()) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *dateTimeBoxElement,
+ aName == nsGkAtoms::value ? u"MozDateTimeValueChanged"_ns
+ : u"MozDateTimeAttributeChanged"_ns,
+ CanBubble::eNo, ChromeOnlyDispatch::eNo);
+ }
+ }
+ }
+ if (needValidityUpdate) {
+ UpdateValidityElementStates(aNotify);
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLInputElement::BeforeSetForm(HTMLFormElement* aForm, bool aBindToTree) {
+ // No need to remove from radio group if we are just binding to tree.
+ if (mType == FormControlType::InputRadio && !aBindToTree) {
+ RemoveFromRadioGroup();
+ }
+
+ // Dispatch event when <input> @form is set
+ if (!aBindToTree) {
+ MaybeDispatchLoginManagerEvents(aForm);
+ }
+}
+
+void HTMLInputElement::AfterClearForm(bool aUnbindOrDelete) {
+ MOZ_ASSERT(!mForm);
+
+ // Do not add back to radio group if we are releasing or unbinding from tree.
+ if (mType == FormControlType::InputRadio && !aUnbindOrDelete &&
+ !GetCurrentRadioGroupContainer()) {
+ AddToRadioGroup();
+ UpdateValueMissingValidityStateForRadio(false);
+ }
+}
+
+void HTMLInputElement::ResultForDialogSubmit(nsAString& aResult) {
+ if (mType == FormControlType::InputImage) {
+ // Get a property set by the frame to find out where it was clicked.
+ const auto* lastClickedPoint =
+ static_cast<CSSIntPoint*>(GetProperty(nsGkAtoms::imageClickedPoint));
+ int32_t x, y;
+ if (lastClickedPoint) {
+ x = lastClickedPoint->x;
+ y = lastClickedPoint->y;
+ } else {
+ x = y = 0;
+ }
+ aResult.AppendInt(x);
+ aResult.AppendLiteral(",");
+ aResult.AppendInt(y);
+ } else {
+ GetAttr(nsGkAtoms::value, aResult);
+ }
+}
+
+void HTMLInputElement::GetAutocomplete(nsAString& aValue) {
+ if (!DoesAutocompleteApply()) {
+ return;
+ }
+
+ aValue.Truncate();
+ const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
+
+ mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute(
+ attributeVal, aValue, mAutocompleteAttrState);
+}
+
+void HTMLInputElement::GetAutocompleteInfo(Nullable<AutocompleteInfo>& aInfo) {
+ if (!DoesAutocompleteApply()) {
+ aInfo.SetNull();
+ return;
+ }
+
+ const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
+ mAutocompleteInfoState = nsContentUtils::SerializeAutocompleteAttribute(
+ attributeVal, aInfo.SetValue(), mAutocompleteInfoState, true);
+}
+
+void HTMLInputElement::GetCapture(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::capture, kCaptureDefault->tag, aValue);
+}
+
+void HTMLInputElement::GetFormEnctype(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::formenctype, "", kFormDefaultEnctype->tag, aValue);
+}
+
+void HTMLInputElement::GetFormMethod(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::formmethod, "", kFormDefaultMethod->tag, aValue);
+}
+
+void HTMLInputElement::GetType(nsAString& aValue) const {
+ GetEnumAttr(nsGkAtoms::type, kInputDefaultType->tag, aValue);
+}
+
+int32_t HTMLInputElement::TabIndexDefault() { return 0; }
+
+uint32_t HTMLInputElement::Height() {
+ if (mType != FormControlType::InputImage) {
+ return 0;
+ }
+ return GetWidthHeightForImage().height;
+}
+
+void HTMLInputElement::SetIndeterminateInternal(bool aValue,
+ bool aShouldInvalidate) {
+ mIndeterminate = aValue;
+ if (mType != FormControlType::InputCheckbox) {
+ return;
+ }
+
+ SetStates(ElementState::INDETERMINATE, aValue);
+
+ if (aShouldInvalidate) {
+ // Repaint the frame
+ if (nsIFrame* frame = GetPrimaryFrame()) {
+ frame->InvalidateFrameSubtree();
+ }
+ }
+}
+
+void HTMLInputElement::SetIndeterminate(bool aValue) {
+ SetIndeterminateInternal(aValue, true);
+}
+
+uint32_t HTMLInputElement::Width() {
+ if (mType != FormControlType::InputImage) {
+ return 0;
+ }
+ return GetWidthHeightForImage().width;
+}
+
+bool HTMLInputElement::SanitizesOnValueGetter() const {
+ // Don't return non-sanitized value for datetime types, email, or number.
+ return mType == FormControlType::InputEmail ||
+ mType == FormControlType::InputNumber || IsDateTimeInputType(mType);
+}
+
+void HTMLInputElement::GetValue(nsAString& aValue, CallerType aCallerType) {
+ GetValueInternal(aValue, aCallerType);
+
+ // In the case where we need to sanitize an input value without affecting
+ // the displayed user's input, we instead sanitize only on .value accesses.
+ // For the more general case of input elements displaying text that isn't
+ // their current value, see bug 805049.
+ if (SanitizesOnValueGetter()) {
+ SanitizeValue(aValue, SanitizationKind::ForValueGetter);
+ }
+}
+
+void HTMLInputElement::GetValueInternal(nsAString& aValue,
+ CallerType aCallerType) const {
+ if (mType != FormControlType::InputFile) {
+ GetNonFileValueInternal(aValue);
+ return;
+ }
+
+ if (aCallerType == CallerType::System) {
+ aValue.Assign(mFileData->mFirstFilePath);
+ return;
+ }
+
+ if (mFileData->mFilesOrDirectories.IsEmpty()) {
+ aValue.Truncate();
+ return;
+ }
+
+ nsAutoString file;
+ GetDOMFileOrDirectoryName(mFileData->mFilesOrDirectories[0], file);
+ if (file.IsEmpty()) {
+ aValue.Truncate();
+ return;
+ }
+
+ aValue.AssignLiteral("C:\\fakepath\\");
+ aValue.Append(file);
+}
+
+void HTMLInputElement::GetNonFileValueInternal(nsAString& aValue) const {
+ switch (GetValueMode()) {
+ case VALUE_MODE_VALUE:
+ if (IsSingleLineTextControl(false)) {
+ if (mInputData.mState) {
+ mInputData.mState->GetValue(aValue, true, /* aForDisplay = */ false);
+ } else {
+ // Value hasn't been set yet.
+ aValue.Truncate();
+ }
+ } else if (!aValue.Assign(mInputData.mValue, fallible)) {
+ aValue.Truncate();
+ }
+ return;
+
+ case VALUE_MODE_FILENAME:
+ MOZ_ASSERT_UNREACHABLE("Someone screwed up here");
+ // We'll just return empty string if someone does screw up.
+ aValue.Truncate();
+ return;
+
+ case VALUE_MODE_DEFAULT:
+ // Treat defaultValue as value.
+ GetAttr(nsGkAtoms::value, aValue);
+ return;
+
+ case VALUE_MODE_DEFAULT_ON:
+ // Treat default value as value and returns "on" if no value.
+ if (!GetAttr(nsGkAtoms::value, aValue)) {
+ aValue.AssignLiteral("on");
+ }
+ return;
+ }
+}
+
+void HTMLInputElement::ClearFiles(bool aSetValueChanged) {
+ nsTArray<OwningFileOrDirectory> data;
+ SetFilesOrDirectories(data, aSetValueChanged);
+}
+
+int32_t HTMLInputElement::MonthsSinceJan1970(uint32_t aYear,
+ uint32_t aMonth) const {
+ return (aYear - 1970) * 12 + aMonth - 1;
+}
+
+/* static */
+Decimal HTMLInputElement::StringToDecimal(const nsAString& aValue) {
+ if (!IsAscii(aValue)) {
+ return Decimal::nan();
+ }
+ NS_LossyConvertUTF16toASCII asciiString(aValue);
+ std::string stdString(asciiString.get(), asciiString.Length());
+ auto decimal = Decimal::fromString(stdString);
+ if (!decimal.isFinite()) {
+ return Decimal::nan();
+ }
+ // Numbers are considered finite IEEE 754 Double-precision floating point
+ // values, but decimal supports a bigger range.
+ static const Decimal maxDouble =
+ Decimal::fromDouble(std::numeric_limits<double>::max());
+ if (decimal < -maxDouble || decimal > maxDouble) {
+ return Decimal::nan();
+ }
+ return decimal;
+}
+
+Decimal HTMLInputElement::GetValueAsDecimal() const {
+ nsAutoString stringValue;
+ GetNonFileValueInternal(stringValue);
+ Decimal result = mInputType->ConvertStringToNumber(stringValue).mResult;
+ return result.isFinite() ? result : Decimal::nan();
+}
+
+void HTMLInputElement::SetValue(const nsAString& aValue, CallerType aCallerType,
+ ErrorResult& aRv) {
+ // check security. Note that setting the value to the empty string is always
+ // OK and gives pages a way to clear a file input if necessary.
+ if (mType == FormControlType::InputFile) {
+ if (!aValue.IsEmpty()) {
+ if (aCallerType != CallerType::System) {
+ // setting the value of a "FILE" input widget requires
+ // chrome privilege
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ Sequence<nsString> list;
+ if (!list.AppendElement(aValue, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ MozSetFileNameArray(list, aRv);
+ return;
+ }
+ ClearFiles(true);
+ } else {
+ if (MayFireChangeOnBlur()) {
+ // If the value has been set by a script, we basically want to keep the
+ // current change event state. If the element is ready to fire a change
+ // event, we should keep it that way. Otherwise, we should make sure the
+ // element will not fire any event because of the script interaction.
+ //
+ // NOTE: this is currently quite expensive work (too much string
+ // manipulation). We should probably optimize that.
+ nsAutoString currentValue;
+ GetNonFileValueInternal(currentValue);
+
+ nsresult rv = SetValueInternal(
+ aValue, &currentValue,
+ {ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ if (mFocusedValue.Equals(currentValue)) {
+ GetValue(mFocusedValue, aCallerType);
+ }
+ } else {
+ nsresult rv = SetValueInternal(
+ aValue,
+ {ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+ }
+ }
+}
+
+HTMLDataListElement* HTMLInputElement::GetList() const {
+ nsAutoString dataListId;
+ GetAttr(nsGkAtoms::list_, dataListId);
+ if (dataListId.IsEmpty()) {
+ return nullptr;
+ }
+
+ DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot();
+ if (!docOrShadow) {
+ return nullptr;
+ }
+
+ return HTMLDataListElement::FromNodeOrNull(
+ docOrShadow->GetElementById(dataListId));
+}
+
+void HTMLInputElement::SetValue(Decimal aValue, CallerType aCallerType) {
+ MOZ_ASSERT(!aValue.isInfinity(), "aValue must not be Infinity!");
+
+ if (aValue.isNaN()) {
+ SetValue(u""_ns, aCallerType, IgnoreErrors());
+ return;
+ }
+
+ nsAutoString value;
+ mInputType->ConvertNumberToString(aValue, value);
+ SetValue(value, aCallerType, IgnoreErrors());
+}
+
+void HTMLInputElement::GetValueAsDate(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aObject,
+ ErrorResult& aRv) {
+ aObject.set(nullptr);
+ if (!IsDateTimeInputType(mType)) {
+ return;
+ }
+
+ Maybe<JS::ClippedTime> time;
+
+ switch (mType) {
+ case FormControlType::InputDate: {
+ uint32_t year, month, day;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseDate(value, &year, &month, &day)) {
+ return;
+ }
+
+ time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, day)));
+ break;
+ }
+ case FormControlType::InputTime: {
+ uint32_t millisecond;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseTime(value, &millisecond)) {
+ return;
+ }
+
+ time.emplace(JS::TimeClip(millisecond));
+ MOZ_ASSERT(time->toDouble() == millisecond,
+ "HTML times are restricted to the day after the epoch and "
+ "never clip");
+ break;
+ }
+ case FormControlType::InputMonth: {
+ uint32_t year, month;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseMonth(value, &year, &month)) {
+ return;
+ }
+
+ time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, 1)));
+ break;
+ }
+ case FormControlType::InputWeek: {
+ uint32_t year, week;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseWeek(value, &year, &week)) {
+ return;
+ }
+
+ double days = DaysSinceEpochFromWeek(year, week);
+ time.emplace(JS::TimeClip(days * kMsPerDay));
+
+ break;
+ }
+ case FormControlType::InputDatetimeLocal: {
+ uint32_t year, month, day, timeInMs;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseDateTimeLocal(value, &year, &month, &day, &timeInMs)) {
+ return;
+ }
+
+ time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs)));
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (time) {
+ aObject.set(JS::NewDateObject(aCx, *time));
+ if (!aObject) {
+ aRv.NoteJSContextException(aCx);
+ }
+ return;
+ }
+
+ MOZ_ASSERT(false, "Unrecognized input type");
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+}
+
+void HTMLInputElement::SetValueAsDate(JSContext* aCx,
+ JS::Handle<JSObject*> aObj,
+ ErrorResult& aRv) {
+ if (!IsDateTimeInputType(mType)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (aObj) {
+ bool isDate;
+ if (!JS::ObjectIsDate(aCx, aObj, &isDate)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+ if (!isDate) {
+ aRv.ThrowTypeError("Value being assigned is not a date.");
+ return;
+ }
+ }
+
+ double milliseconds;
+ if (aObj) {
+ if (!js::DateGetMsecSinceEpoch(aCx, aObj, &milliseconds)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+ } else {
+ milliseconds = UnspecifiedNaN<double>();
+ }
+
+ // At this point we know we're not a file input, so we can just pass "not
+ // system" as the caller type, since the caller type only matters in the file
+ // input case.
+ if (std::isnan(milliseconds)) {
+ SetValue(u""_ns, CallerType::NonSystem, aRv);
+ return;
+ }
+
+ if (mType != FormControlType::InputMonth) {
+ SetValue(Decimal::fromDouble(milliseconds), CallerType::NonSystem);
+ return;
+ }
+
+ // type=month expects the value to be number of months.
+ double year = JS::YearFromTime(milliseconds);
+ double month = JS::MonthFromTime(milliseconds);
+
+ if (std::isnan(year) || std::isnan(month)) {
+ SetValue(u""_ns, CallerType::NonSystem, aRv);
+ return;
+ }
+
+ int32_t months = MonthsSinceJan1970(year, month + 1);
+ SetValue(Decimal(int32_t(months)), CallerType::NonSystem);
+}
+
+void HTMLInputElement::SetValueAsNumber(double aValueAsNumber,
+ ErrorResult& aRv) {
+ // TODO: return TypeError when HTMLInputElement is converted to WebIDL, see
+ // bug 825197.
+ if (std::isinf(aValueAsNumber)) {
+ aRv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ if (!DoesValueAsNumberApply()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // At this point we know we're not a file input, so we can just pass "not
+ // system" as the caller type, since the caller type only matters in the file
+ // input case.
+ SetValue(Decimal::fromDouble(aValueAsNumber), CallerType::NonSystem);
+}
+
+Decimal HTMLInputElement::GetMinimum() const {
+ MOZ_ASSERT(
+ DoesValueAsNumberApply(),
+ "GetMinimum() should only be used for types that allow .valueAsNumber");
+
+ // Only type=range has a default minimum
+ Decimal defaultMinimum =
+ mType == FormControlType::InputRange ? Decimal(0) : Decimal::nan();
+
+ if (!HasAttr(nsGkAtoms::min)) {
+ return defaultMinimum;
+ }
+
+ nsAutoString minStr;
+ GetAttr(nsGkAtoms::min, minStr);
+
+ Decimal min = mInputType->ConvertStringToNumber(minStr).mResult;
+ return min.isFinite() ? min : defaultMinimum;
+}
+
+Decimal HTMLInputElement::GetMaximum() const {
+ MOZ_ASSERT(
+ DoesValueAsNumberApply(),
+ "GetMaximum() should only be used for types that allow .valueAsNumber");
+
+ // Only type=range has a default maximum
+ Decimal defaultMaximum =
+ mType == FormControlType::InputRange ? Decimal(100) : Decimal::nan();
+
+ if (!HasAttr(nsGkAtoms::max)) {
+ return defaultMaximum;
+ }
+
+ nsAutoString maxStr;
+ GetAttr(nsGkAtoms::max, maxStr);
+
+ Decimal max = mInputType->ConvertStringToNumber(maxStr).mResult;
+ return max.isFinite() ? max : defaultMaximum;
+}
+
+Decimal HTMLInputElement::GetStepBase() const {
+ MOZ_ASSERT(IsDateTimeInputType(mType) ||
+ mType == FormControlType::InputNumber ||
+ mType == FormControlType::InputRange,
+ "Check that kDefaultStepBase is correct for this new type");
+ // Do NOT use GetMinimum here - the spec says to use "the min content
+ // attribute", not "the minimum".
+ nsAutoString minStr;
+ if (GetAttr(nsGkAtoms::min, minStr)) {
+ Decimal min = mInputType->ConvertStringToNumber(minStr).mResult;
+ if (min.isFinite()) {
+ return min;
+ }
+ }
+
+ // If @min is not a double, we should use @value.
+ nsAutoString valueStr;
+ if (GetAttr(nsGkAtoms::value, valueStr)) {
+ Decimal value = mInputType->ConvertStringToNumber(valueStr).mResult;
+ if (value.isFinite()) {
+ return value;
+ }
+ }
+
+ if (mType == FormControlType::InputWeek) {
+ return kDefaultStepBaseWeek;
+ }
+
+ return kDefaultStepBase;
+}
+
+nsresult HTMLInputElement::GetValueIfStepped(int32_t aStep,
+ StepCallerType aCallerType,
+ Decimal* aNextStep) {
+ if (!DoStepDownStepUpApply()) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ Decimal stepBase = GetStepBase();
+ Decimal step = GetStep();
+ if (step == kStepAny) {
+ if (aCallerType != CALLED_FOR_USER_EVENT) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+ // Allow the spin buttons and up/down arrow keys to do something sensible:
+ step = GetDefaultStep();
+ }
+
+ Decimal minimum = GetMinimum();
+ Decimal maximum = GetMaximum();
+
+ if (!maximum.isNaN()) {
+ // "max - (max - stepBase) % step" is the nearest valid value to max.
+ maximum = maximum - NS_floorModulo(maximum - stepBase, step);
+ if (!minimum.isNaN()) {
+ if (minimum > maximum) {
+ // Either the minimum was greater than the maximum prior to our
+ // adjustment to align maximum on a step, or else (if we adjusted
+ // maximum) there is no valid step between minimum and the unadjusted
+ // maximum.
+ return NS_OK;
+ }
+ }
+ }
+
+ Decimal value = GetValueAsDecimal();
+ bool valueWasNaN = false;
+ if (value.isNaN()) {
+ value = Decimal(0);
+ valueWasNaN = true;
+ }
+ Decimal valueBeforeStepping = value;
+
+ Decimal deltaFromStep = NS_floorModulo(value - stepBase, step);
+
+ if (deltaFromStep != Decimal(0)) {
+ if (aStep > 0) {
+ value += step - deltaFromStep; // partial step
+ value += step * Decimal(aStep - 1); // then remaining steps
+ } else if (aStep < 0) {
+ value -= deltaFromStep; // partial step
+ value += step * Decimal(aStep + 1); // then remaining steps
+ }
+ } else {
+ value += step * Decimal(aStep);
+ }
+
+ if (value < minimum) {
+ value = minimum;
+ deltaFromStep = NS_floorModulo(value - stepBase, step);
+ if (deltaFromStep != Decimal(0)) {
+ value += step - deltaFromStep;
+ }
+ }
+ if (value > maximum) {
+ value = maximum;
+ deltaFromStep = NS_floorModulo(value - stepBase, step);
+ if (deltaFromStep != Decimal(0)) {
+ value -= deltaFromStep;
+ }
+ }
+
+ if (!valueWasNaN && // value="", resulting in us using "0"
+ ((aStep > 0 && value < valueBeforeStepping) ||
+ (aStep < 0 && value > valueBeforeStepping))) {
+ // We don't want step-up to effectively step down, or step-down to
+ // effectively step up, so return;
+ return NS_OK;
+ }
+
+ *aNextStep = value;
+ return NS_OK;
+}
+
+nsresult HTMLInputElement::ApplyStep(int32_t aStep) {
+ Decimal nextStep = Decimal::nan(); // unchanged if value will not change
+
+ nsresult rv = GetValueIfStepped(aStep, CALLED_FOR_SCRIPT, &nextStep);
+
+ if (NS_SUCCEEDED(rv) && nextStep.isFinite()) {
+ // We know we're not a file input, so the caller type does not matter; just
+ // pass "not system" to be safe.
+ SetValue(nextStep, CallerType::NonSystem);
+ }
+
+ return rv;
+}
+
+bool HTMLInputElement::IsDateTimeInputType(FormControlType aType) {
+ switch (aType) {
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+ default:
+ return false;
+ }
+}
+
+void HTMLInputElement::MozGetFileNameArray(nsTArray<nsString>& aArray,
+ ErrorResult& aRv) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ const nsTArray<OwningFileOrDirectory>& filesOrDirs =
+ GetFilesOrDirectoriesInternal();
+ for (uint32_t i = 0; i < filesOrDirs.Length(); i++) {
+ nsAutoString str;
+ GetDOMFileOrDirectoryPath(filesOrDirs[i], str, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ aArray.AppendElement(str);
+ }
+}
+
+void HTMLInputElement::MozSetFileArray(
+ const Sequence<OwningNonNull<File>>& aFiles) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ MOZ_ASSERT(global);
+ if (!global) {
+ return;
+ }
+
+ nsTArray<OwningFileOrDirectory> files;
+ for (uint32_t i = 0; i < aFiles.Length(); ++i) {
+ RefPtr<File> file = File::Create(global, aFiles[i].get()->Impl());
+ if (NS_WARN_IF(!file)) {
+ return;
+ }
+
+ OwningFileOrDirectory* element = files.AppendElement();
+ element->SetAsFile() = file;
+ }
+
+ SetFilesOrDirectories(files, true);
+}
+
+void HTMLInputElement::MozSetFileNameArray(const Sequence<nsString>& aFileNames,
+ ErrorResult& aRv) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ if (XRE_IsContentProcess()) {
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return;
+ }
+
+ nsTArray<OwningFileOrDirectory> files;
+ for (uint32_t i = 0; i < aFileNames.Length(); ++i) {
+ nsCOMPtr<nsIFile> file;
+
+ if (StringBeginsWith(aFileNames[i], u"file:"_ns,
+ nsASCIICaseInsensitiveStringComparator)) {
+ // Converts the URL string into the corresponding nsIFile if possible
+ // A local file will be created if the URL string begins with file://
+ NS_GetFileFromURLSpec(NS_ConvertUTF16toUTF8(aFileNames[i]),
+ getter_AddRefs(file));
+ }
+
+ if (!file) {
+ // this is no "file://", try as local file
+ NS_NewLocalFile(aFileNames[i], false, getter_AddRefs(file));
+ }
+
+ if (!file) {
+ continue; // Not much we can do if the file doesn't exist
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RefPtr<File> domFile = File::CreateFromFile(global, file);
+ if (NS_WARN_IF(!domFile)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ OwningFileOrDirectory* element = files.AppendElement();
+ element->SetAsFile() = domFile;
+ }
+
+ SetFilesOrDirectories(files, true);
+}
+
+void HTMLInputElement::MozSetDirectory(const nsAString& aDirectoryPath,
+ ErrorResult& aRv) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ nsCOMPtr<nsIFile> file;
+ aRv = NS_NewLocalFile(aDirectoryPath, true, getter_AddRefs(file));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (NS_WARN_IF(!window)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RefPtr<Directory> directory = Directory::Create(window->AsGlobal(), file);
+ MOZ_ASSERT(directory);
+
+ nsTArray<OwningFileOrDirectory> array;
+ OwningFileOrDirectory* element = array.AppendElement();
+ element->SetAsDirectory() = directory;
+
+ SetFilesOrDirectories(array, true);
+}
+
+void HTMLInputElement::GetDateTimeInputBoxValue(DateTimeValue& aValue) {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType)) || !mDateTimeInputBoxValue) {
+ return;
+ }
+
+ aValue = *mDateTimeInputBoxValue;
+}
+
+Element* HTMLInputElement::GetDateTimeBoxElement() {
+ if (!GetShadowRoot()) {
+ return nullptr;
+ }
+
+ // The datetimebox <div> is the only child of the UA Widget Shadow Root
+ // if it is present.
+ MOZ_ASSERT(GetShadowRoot()->IsUAWidget());
+ MOZ_ASSERT(1 >= GetShadowRoot()->GetChildCount());
+ if (nsIContent* inputAreaContent = GetShadowRoot()->GetFirstChild()) {
+ return inputAreaContent->AsElement();
+ }
+
+ return nullptr;
+}
+
+void HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue) {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+
+ mDateTimeInputBoxValue = MakeUnique<DateTimeValue>(aInitialValue);
+ nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this),
+ u"MozOpenDateTimePicker"_ns,
+ CanBubble::eYes, Cancelable::eYes);
+}
+
+void HTMLInputElement::UpdateDateTimePicker(const DateTimeValue& aValue) {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+
+ mDateTimeInputBoxValue = MakeUnique<DateTimeValue>(aValue);
+ nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this),
+ u"MozUpdateDateTimePicker"_ns,
+ CanBubble::eYes, Cancelable::eYes);
+}
+
+void HTMLInputElement::CloseDateTimePicker() {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+
+ nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this),
+ u"MozCloseDateTimePicker"_ns,
+ CanBubble::eYes, Cancelable::eYes);
+}
+
+void HTMLInputElement::SetFocusState(bool aIsFocused) {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+ SetStates(ElementState::FOCUS | ElementState::FOCUSRING, aIsFocused);
+}
+
+void HTMLInputElement::UpdateValidityState() {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+
+ // For now, datetime input box call this function only when the value may
+ // become valid/invalid. For other validity states, they will be updated when
+ // .value is actually changed.
+ UpdateBadInputValidityState();
+ UpdateValidityElementStates(true);
+}
+
+bool HTMLInputElement::MozIsTextField(bool aExcludePassword) {
+ // TODO: temporary until bug 888320 is fixed.
+ //
+ // FIXME: Historically we never returned true for `number`, we should consider
+ // changing that now that it is similar to other inputs.
+ if (IsDateTimeInputType(mType) || mType == FormControlType::InputNumber) {
+ return false;
+ }
+
+ return IsSingleLineTextControl(aExcludePassword);
+}
+
+void HTMLInputElement::SetUserInput(const nsAString& aValue,
+ nsIPrincipal& aSubjectPrincipal) {
+ AutoHandlingUserInputStatePusher inputStatePusher(true);
+
+ if (mType == FormControlType::InputFile &&
+ !aSubjectPrincipal.IsSystemPrincipal()) {
+ return;
+ }
+
+ if (mType == FormControlType::InputFile) {
+ Sequence<nsString> list;
+ if (!list.AppendElement(aValue, fallible)) {
+ return;
+ }
+
+ MozSetFileNameArray(list, IgnoreErrors());
+ return;
+ }
+
+ bool isInputEventDispatchedByTextControlState =
+ GetValueMode() == VALUE_MODE_VALUE && IsSingleLineTextControl(false);
+
+ nsresult rv = SetValueInternal(
+ aValue,
+ {ValueSetterOption::BySetUserInputAPI, ValueSetterOption::SetValueChanged,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ if (!isInputEventDispatchedByTextControlState) {
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ }
+
+ // If this element is not currently focused, it won't receive a change event
+ // for this update through the normal channels. So fire a change event
+ // immediately, instead.
+ if (CreatesDateTimeWidget() || !ShouldBlur(this)) {
+ FireChangeEventIfNeeded();
+ }
+}
+
+nsIEditor* HTMLInputElement::GetEditorForBindings() {
+ if (!GetPrimaryFrame()) {
+ // Ensure we construct frames (and thus an editor) if needed.
+ GetPrimaryFrame(FlushType::Frames);
+ }
+ return GetTextEditorFromState();
+}
+
+bool HTMLInputElement::HasEditor() const {
+ return !!GetTextEditorWithoutCreation();
+}
+
+TextEditor* HTMLInputElement::GetTextEditorFromState() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->GetTextEditor();
+ }
+ return nullptr;
+}
+
+TextEditor* HTMLInputElement::GetTextEditor() {
+ return GetTextEditorFromState();
+}
+
+TextEditor* HTMLInputElement::GetTextEditorWithoutCreation() const {
+ TextControlState* state = GetEditorState();
+ if (!state) {
+ return nullptr;
+ }
+ return state->GetTextEditorWithoutCreation();
+}
+
+nsISelectionController* HTMLInputElement::GetSelectionController() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->GetSelectionController();
+ }
+ return nullptr;
+}
+
+nsFrameSelection* HTMLInputElement::GetConstFrameSelection() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->GetConstFrameSelection();
+ }
+ return nullptr;
+}
+
+nsresult HTMLInputElement::BindToFrame(nsTextControlFrame* aFrame) {
+ MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript());
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->BindToFrame(aFrame);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+void HTMLInputElement::UnbindFromFrame(nsTextControlFrame* aFrame) {
+ TextControlState* state = GetEditorState();
+ if (state && aFrame) {
+ state->UnbindFromFrame(aFrame);
+ }
+}
+
+nsresult HTMLInputElement::CreateEditor() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->PrepareEditor();
+ }
+ return NS_ERROR_FAILURE;
+}
+
+void HTMLInputElement::SetPreviewValue(const nsAString& aValue) {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ state->SetPreviewText(aValue, true);
+ }
+}
+
+void HTMLInputElement::GetPreviewValue(nsAString& aValue) {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ state->GetPreviewText(aValue);
+ }
+}
+
+void HTMLInputElement::EnablePreview() {
+ if (mIsPreviewEnabled) {
+ return;
+ }
+
+ mIsPreviewEnabled = true;
+ // Reconstruct the frame to append an anonymous preview node
+ nsLayoutUtils::PostRestyleEvent(this, RestyleHint{0},
+ nsChangeHint_ReconstructFrame);
+}
+
+bool HTMLInputElement::IsPreviewEnabled() { return mIsPreviewEnabled; }
+
+void HTMLInputElement::GetDisplayFileName(nsAString& aValue) const {
+ MOZ_ASSERT(mFileData);
+
+ if (OwnerDoc()->IsStaticDocument()) {
+ aValue = mFileData->mStaticDocFileList;
+ return;
+ }
+
+ if (mFileData->mFilesOrDirectories.Length() == 1) {
+ GetDOMFileOrDirectoryName(mFileData->mFilesOrDirectories[0], aValue);
+ return;
+ }
+
+ nsAutoString value;
+
+ if (mFileData->mFilesOrDirectories.IsEmpty()) {
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ HasAttr(nsGkAtoms::webkitdirectory)) {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "NoDirSelected", OwnerDoc(),
+ value);
+ } else if (HasAttr(nsGkAtoms::multiple)) {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "NoFilesSelected", OwnerDoc(),
+ value);
+ } else {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "NoFileSelected", OwnerDoc(),
+ value);
+ }
+ } else {
+ nsString count;
+ count.AppendInt(int(mFileData->mFilesOrDirectories.Length()));
+
+ nsContentUtils::FormatMaybeLocalizedString(
+ value, nsContentUtils::eFORMS_PROPERTIES, "XFilesSelected", OwnerDoc(),
+ count);
+ }
+
+ aValue = value;
+}
+
+const nsTArray<OwningFileOrDirectory>&
+HTMLInputElement::GetFilesOrDirectoriesInternal() const {
+ return mFileData->mFilesOrDirectories;
+}
+
+void HTMLInputElement::SetFilesOrDirectories(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories,
+ bool aSetValueChanged) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ MOZ_ASSERT(mFileData);
+
+ mFileData->ClearGetFilesHelpers();
+
+ if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) {
+ HTMLInputElement_Binding::ClearCachedWebkitEntriesValue(this);
+ mFileData->mEntries.Clear();
+ }
+
+ mFileData->mFilesOrDirectories.Clear();
+ mFileData->mFilesOrDirectories.AppendElements(aFilesOrDirectories);
+
+ AfterSetFilesOrDirectories(aSetValueChanged);
+}
+
+void HTMLInputElement::SetFiles(FileList* aFiles, bool aSetValueChanged) {
+ MOZ_ASSERT(mFileData);
+
+ mFileData->mFilesOrDirectories.Clear();
+ mFileData->ClearGetFilesHelpers();
+
+ if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) {
+ HTMLInputElement_Binding::ClearCachedWebkitEntriesValue(this);
+ mFileData->mEntries.Clear();
+ }
+
+ if (aFiles) {
+ uint32_t listLength = aFiles->Length();
+ for (uint32_t i = 0; i < listLength; i++) {
+ OwningFileOrDirectory* element =
+ mFileData->mFilesOrDirectories.AppendElement();
+ element->SetAsFile() = aFiles->Item(i);
+ }
+ }
+
+ AfterSetFilesOrDirectories(aSetValueChanged);
+}
+
+// This method is used for testing only.
+void HTMLInputElement::MozSetDndFilesAndDirectories(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ SetFilesOrDirectories(aFilesOrDirectories, true);
+
+ if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) {
+ UpdateEntries(aFilesOrDirectories);
+ }
+
+ RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
+ new DispatchChangeEventCallback(this);
+
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ HasAttr(nsGkAtoms::webkitdirectory)) {
+ ErrorResult rv;
+ GetFilesHelper* helper =
+ GetOrCreateGetFilesHelper(true /* recursionFlag */, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return;
+ }
+
+ helper->AddCallback(dispatchChangeEventCallback);
+ } else {
+ dispatchChangeEventCallback->DispatchEvents();
+ }
+}
+
+void HTMLInputElement::AfterSetFilesOrDirectories(bool aSetValueChanged) {
+ // No need to flush here, if there's no frame at this point we
+ // don't need to force creation of one just to tell it about this
+ // new value. We just want the display to update as needed.
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(false);
+ if (formControlFrame) {
+ nsAutoString readableValue;
+ GetDisplayFileName(readableValue);
+ formControlFrame->SetFormProperty(nsGkAtoms::value, readableValue);
+ }
+
+ // Grab the full path here for any chrome callers who access our .value via a
+ // CPOW. This path won't be called from a CPOW meaning the potential sync IPC
+ // call under GetMozFullPath won't be rejected for not being urgent.
+ if (mFileData->mFilesOrDirectories.IsEmpty()) {
+ mFileData->mFirstFilePath.Truncate();
+ } else {
+ ErrorResult rv;
+ GetDOMFileOrDirectoryPath(mFileData->mFilesOrDirectories[0],
+ mFileData->mFirstFilePath, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ }
+ }
+
+ // Null out |mFileData->mFileList| to return a new file list when asked for.
+ // Don't clear it since the file list might come from the user via SetFiles.
+ if (mFileData->mFileList) {
+ mFileData->mFileList = nullptr;
+ }
+
+ if (aSetValueChanged) {
+ SetValueChanged(true);
+ }
+
+ UpdateAllValidityStates(true);
+}
+
+void HTMLInputElement::FireChangeEventIfNeeded() {
+ if (!MayFireChangeOnBlur()) {
+ return;
+ }
+
+ // We're not exposing the GetValue return value anywhere here, so it's safe to
+ // claim to be a system caller.
+ nsAutoString value;
+ GetValue(value, CallerType::System);
+
+ // NOTE(emilio): Per spec we should not set this if we don't fire the change
+ // event, but that seems like a bug. Using mValueChanged seems reasonable to
+ // keep the expected behavior while
+ // https://github.com/whatwg/html/issues/10013 is resolved.
+ if (mValueChanged) {
+ SetUserInteracted(true);
+ }
+ if (mFocusedValue.Equals(value)) {
+ return;
+ }
+ // Dispatch the change event.
+ mFocusedValue = value;
+ nsContentUtils::DispatchTrustedEvent(
+ OwnerDoc(), static_cast<nsIContent*>(this), u"change"_ns, CanBubble::eYes,
+ Cancelable::eNo);
+}
+
+FileList* HTMLInputElement::GetFiles() {
+ if (mType != FormControlType::InputFile) {
+ return nullptr;
+ }
+
+ if (!mFileData->mFileList) {
+ mFileData->mFileList = new FileList(static_cast<nsIContent*>(this));
+ for (const OwningFileOrDirectory& item : GetFilesOrDirectoriesInternal()) {
+ if (item.IsFile()) {
+ mFileData->mFileList->Append(item.GetAsFile());
+ }
+ }
+ }
+
+ return mFileData->mFileList;
+}
+
+void HTMLInputElement::SetFiles(FileList* aFiles) {
+ if (mType != FormControlType::InputFile || !aFiles) {
+ return;
+ }
+
+ // Update |mFileData->mFilesOrDirectories|
+ SetFiles(aFiles, true);
+
+ MOZ_ASSERT(!mFileData->mFileList, "Should've cleared the existing file list");
+
+ // Update |mFileData->mFileList| without copy
+ mFileData->mFileList = aFiles;
+}
+
+/* static */
+void HTMLInputElement::HandleNumberControlSpin(void* aData) {
+ RefPtr<HTMLInputElement> input = static_cast<HTMLInputElement*>(aData);
+
+ NS_ASSERTION(input->mNumberControlSpinnerIsSpinning,
+ "Should have called nsRepeatService::Stop()");
+
+ nsNumberControlFrame* numberControlFrame =
+ do_QueryFrame(input->GetPrimaryFrame());
+ if (input->mType != FormControlType::InputNumber || !numberControlFrame) {
+ // Type has changed (and possibly our frame type hasn't been updated yet)
+ // or else we've lost our frame. Either way, stop the timer and don't do
+ // anything else.
+ input->StopNumberControlSpinnerSpin();
+ } else {
+ input->StepNumberControlForUserEvent(
+ input->mNumberControlSpinnerSpinsUp ? 1 : -1);
+ }
+}
+
+nsresult HTMLInputElement::SetValueInternal(
+ const nsAString& aValue, const nsAString* aOldValue,
+ const ValueSetterOptions& aOptions) {
+ MOZ_ASSERT(GetValueMode() != VALUE_MODE_FILENAME,
+ "Don't call SetValueInternal for file inputs");
+
+ // We want to remember if the SetValueInternal() call is being made for a XUL
+ // element. We do that by looking at the parent node here, and if that node
+ // is a XUL node, we consider our control a XUL control. XUL controls preserve
+ // edit history across value setters.
+ //
+ // TODO(emilio): Rather than doing this maybe add an attribute instead and
+ // read it only on chrome docs or something? That'd allow front-end code to
+ // move away from xul without weird side-effects.
+ const bool forcePreserveUndoHistory = mParent && mParent->IsXULElement();
+
+ switch (GetValueMode()) {
+ case VALUE_MODE_VALUE: {
+ // At the moment, only single line text control have to sanitize their
+ // value Because we have to create a new string for that, we should
+ // prevent doing it if it's useless.
+ nsAutoString value(aValue);
+
+ if (mDoneCreating &&
+ !(mType == FormControlType::InputNumber &&
+ aOptions.contains(ValueSetterOption::BySetUserInputAPI))) {
+ // When the value of a number input is set by a script, we need to make
+ // sure the value is a valid floating-point number.
+ // https://html.spec.whatwg.org/#valid-floating-point-number
+ // When it's set by a user, however, we need to be more permissive, so
+ // we don't sanitize its value here. See bug 1839572.
+ SanitizeValue(value, SanitizationKind::ForValueSetter);
+ }
+ // else DoneCreatingElement calls us again once mDoneCreating is true
+
+ const bool setValueChanged =
+ aOptions.contains(ValueSetterOption::SetValueChanged);
+ if (setValueChanged) {
+ SetValueChanged(true);
+ }
+
+ if (IsSingleLineTextControl(false)) {
+ // Note that if aOptions includes
+ // ValueSetterOption::BySetUserInputAPI, "input" event is automatically
+ // dispatched by TextControlState::SetValue(). If you'd change condition
+ // of calling this method, you need to maintain SetUserInput() too. FYI:
+ // After calling SetValue(), the input type might have been
+ // modified so that mInputData may not store TextControlState.
+ EnsureEditorState();
+ if (!mInputData.mState->SetValue(
+ value, aOldValue,
+ forcePreserveUndoHistory
+ ? aOptions + ValueSetterOption::PreserveUndoHistory
+ : aOptions)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ // If the caller won't dispatch "input" event via
+ // nsContentUtils::DispatchInputEvent(), we need to modify
+ // validationMessage value here.
+ //
+ // FIXME(emilio): ValueSetterOption::ByInternalAPI is not supposed to
+ // change state, but maybe we could run this too?
+ if (aOptions.contains(ValueSetterOption::ByContentAPI)) {
+ MaybeUpdateAllValidityStates(!mDoneCreating);
+ }
+ } else {
+ free(mInputData.mValue);
+ mInputData.mValue = ToNewUnicode(value);
+ if (setValueChanged) {
+ SetValueChanged(true);
+ }
+ if (mType == FormControlType::InputRange) {
+ nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame());
+ if (frame) {
+ frame->UpdateForValueChange();
+ }
+ } else if (CreatesDateTimeWidget() &&
+ !aOptions.contains(ValueSetterOption::BySetUserInputAPI)) {
+ if (Element* dateTimeBoxElement = GetDateTimeBoxElement()) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *dateTimeBoxElement, u"MozDateTimeValueChanged"_ns,
+ CanBubble::eNo, ChromeOnlyDispatch::eNo);
+ }
+ }
+ if (mDoneCreating) {
+ OnValueChanged(ValueChangeKind::Internal, value.IsEmpty(), &value);
+ }
+ // else DoneCreatingElement calls us again once mDoneCreating is true
+ }
+
+ if (mType == FormControlType::InputColor) {
+ // Update color frame, to reflect color changes
+ nsColorControlFrame* colorControlFrame =
+ do_QueryFrame(GetPrimaryFrame());
+ if (colorControlFrame) {
+ colorControlFrame->UpdateColor();
+ }
+ }
+ return NS_OK;
+ }
+
+ case VALUE_MODE_DEFAULT:
+ case VALUE_MODE_DEFAULT_ON:
+ // If the value of a hidden input was changed, we mark it changed so that
+ // we will know we need to save / restore the value. Yes, we are
+ // overloading the meaning of ValueChanged just a teensy bit to save a
+ // measly byte of storage space in HTMLInputElement. Yes, you are free to
+ // make a new flag, NEED_TO_SAVE_VALUE, at such time as mBitField becomes
+ // a 16-bit value.
+ if (mType == FormControlType::InputHidden) {
+ SetValueChanged(true);
+ }
+
+ // Make sure to keep track of the last value change not being interactive,
+ // just in case this used to be another kind of editable input before.
+ // Note that a checked change _could_ really be interactive, but we don't
+ // keep track of that elsewhere so seems fine to just do this.
+ SetLastValueChangeWasInteractive(false);
+
+ // Treat value == defaultValue for other input elements.
+ return nsGenericHTMLFormControlElementWithState::SetAttr(
+ kNameSpaceID_None, nsGkAtoms::value, aValue, true);
+
+ case VALUE_MODE_FILENAME:
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // This return statement is required for some compilers.
+ return NS_OK;
+}
+
+void HTMLInputElement::SetValueChanged(bool aValueChanged) {
+ if (mValueChanged == aValueChanged) {
+ return;
+ }
+ mValueChanged = aValueChanged;
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ UpdateValidityElementStates(true);
+}
+
+void HTMLInputElement::SetLastValueChangeWasInteractive(bool aWasInteractive) {
+ if (aWasInteractive == mLastValueChangeWasInteractive) {
+ return;
+ }
+ mLastValueChangeWasInteractive = aWasInteractive;
+ const bool wasValid = IsValid();
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ if (wasValid != IsValid()) {
+ UpdateValidityElementStates(true);
+ }
+}
+
+void HTMLInputElement::SetCheckedChanged(bool aCheckedChanged) {
+ DoSetCheckedChanged(aCheckedChanged, true);
+}
+
+void HTMLInputElement::DoSetCheckedChanged(bool aCheckedChanged, bool aNotify) {
+ if (mType == FormControlType::InputRadio) {
+ if (mCheckedChanged != aCheckedChanged) {
+ nsCOMPtr<nsIRadioVisitor> visitor =
+ new nsRadioSetCheckedChangedVisitor(aCheckedChanged);
+ VisitGroup(visitor);
+ }
+ } else {
+ SetCheckedChangedInternal(aCheckedChanged);
+ }
+}
+
+void HTMLInputElement::SetCheckedChangedInternal(bool aCheckedChanged) {
+ if (mCheckedChanged == aCheckedChanged) {
+ return;
+ }
+ mCheckedChanged = aCheckedChanged;
+ UpdateValidityElementStates(true);
+}
+
+void HTMLInputElement::SetChecked(bool aChecked) {
+ DoSetChecked(aChecked, true, true);
+}
+
+void HTMLInputElement::DoSetChecked(bool aChecked, bool aNotify,
+ bool aSetValueChanged) {
+ // If the user or JS attempts to set checked, whether it actually changes the
+ // value or not, we say the value was changed so that defaultValue don't
+ // affect it no more.
+ if (aSetValueChanged) {
+ DoSetCheckedChanged(true, aNotify);
+ }
+
+ // Don't do anything if we're not changing whether it's checked (it would
+ // screw up state actually, especially when you are setting radio button to
+ // false)
+ if (mChecked == aChecked) {
+ return;
+ }
+
+ // Set checked
+ if (mType != FormControlType::InputRadio) {
+ SetCheckedInternal(aChecked, aNotify);
+ return;
+ }
+
+ // For radio button, we need to do some extra fun stuff
+ if (aChecked) {
+ RadioSetChecked(aNotify);
+ return;
+ }
+
+ if (auto* container = GetCurrentRadioGroupContainer()) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ container->SetCurrentRadioButton(name, nullptr);
+ }
+ // SetCheckedInternal is going to ask all radios to update their
+ // validity state. We have to be sure the radio group container knows
+ // the currently selected radio.
+ SetCheckedInternal(false, aNotify);
+}
+
+void HTMLInputElement::RadioSetChecked(bool aNotify) {
+ // Find the selected radio button so we can deselect it
+ HTMLInputElement* currentlySelected = GetSelectedRadioButton();
+
+ // Deselect the currently selected radio button
+ if (currentlySelected) {
+ // Pass true for the aNotify parameter since the currently selected
+ // button is already in the document.
+ currentlySelected->SetCheckedInternal(false, true);
+ }
+
+ // Let the group know that we are now the One True Radio Button
+ if (auto* container = GetCurrentRadioGroupContainer()) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ container->SetCurrentRadioButton(name, this);
+ }
+
+ // SetCheckedInternal is going to ask all radios to update their
+ // validity state.
+ SetCheckedInternal(true, aNotify);
+}
+
+RadioGroupContainer* HTMLInputElement::GetCurrentRadioGroupContainer() const {
+ NS_ASSERTION(
+ mType == FormControlType::InputRadio,
+ "GetRadioGroupContainer should only be called when type='radio'");
+ return mRadioGroupContainer;
+}
+
+RadioGroupContainer* HTMLInputElement::FindTreeRadioGroupContainer() const {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ if (name.IsEmpty()) {
+ return nullptr;
+ }
+ if (mForm) {
+ return &mForm->OwnedRadioGroupContainer();
+ }
+ if (IsInNativeAnonymousSubtree()) {
+ return nullptr;
+ }
+ if (Document* doc = GetUncomposedDoc()) {
+ return &doc->OwnedRadioGroupContainer();
+ }
+ return &static_cast<FragmentOrElement*>(SubtreeRoot())
+ ->OwnedRadioGroupContainer();
+}
+
+void HTMLInputElement::DisconnectRadioGroupContainer() {
+ mRadioGroupContainer = nullptr;
+}
+
+HTMLInputElement* HTMLInputElement::GetSelectedRadioButton() const {
+ auto* container = GetCurrentRadioGroupContainer();
+ if (!container) {
+ return nullptr;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ return container->GetCurrentRadioButton(name);
+}
+
+void HTMLInputElement::MaybeSubmitForm(nsPresContext* aPresContext) {
+ if (!mForm) {
+ // Nothing to do here.
+ return;
+ }
+
+ RefPtr<PresShell> presShell = aPresContext->GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ // Get the default submit element
+ if (RefPtr<nsGenericHTMLFormElement> submitContent =
+ mForm->GetDefaultSubmitElement()) {
+ WidgetMouseEvent event(true, eMouseClick, nullptr, WidgetMouseEvent::eReal);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ presShell->HandleDOMEventWithTarget(submitContent, &event, &status);
+ } else if (!mForm->ImplicitSubmissionIsDisabled()) {
+ // If there's only one text control, just submit the form
+ // Hold strong ref across the event
+ RefPtr<dom::HTMLFormElement> form(mForm);
+ form->MaybeSubmit(nullptr);
+ }
+}
+
+void HTMLInputElement::UpdateCheckedState(bool aNotify) {
+ SetStates(ElementState::CHECKED, IsRadioOrCheckbox() && mChecked, aNotify);
+}
+
+void HTMLInputElement::UpdateIndeterminateState(bool aNotify) {
+ bool indeterminate = [&] {
+ if (mType == FormControlType::InputCheckbox) {
+ return mIndeterminate;
+ }
+ if (mType == FormControlType::InputRadio) {
+ return !mChecked && !GetSelectedRadioButton();
+ }
+ return false;
+ }();
+ SetStates(ElementState::INDETERMINATE, indeterminate, aNotify);
+}
+
+void HTMLInputElement::SetCheckedInternal(bool aChecked, bool aNotify) {
+ // Set the value
+ mChecked = aChecked;
+
+ if (IsRadioOrCheckbox()) {
+ SetStates(ElementState::CHECKED, aChecked, aNotify);
+ }
+
+ // No need to update element state, since we're about to call
+ // UpdateState anyway.
+ UpdateAllValidityStatesButNotElementState();
+ UpdateIndeterminateState(aNotify);
+ UpdateValidityElementStates(aNotify);
+
+ // Notify all radios in the group that value has changed, this is to let
+ // radios to have the chance to update its states, e.g., :indeterminate.
+ if (mType == FormControlType::InputRadio) {
+ nsCOMPtr<nsIRadioVisitor> visitor = new nsRadioUpdateStateVisitor(this);
+ VisitGroup(visitor);
+ }
+}
+
+#if !defined(ANDROID) && !defined(XP_MACOSX)
+bool HTMLInputElement::IsNodeApzAwareInternal() const {
+ // Tell APZC we may handle mouse wheel event and do preventDefault when input
+ // type is number.
+ return mType == FormControlType::InputNumber ||
+ mType == FormControlType::InputRange ||
+ nsINode::IsNodeApzAwareInternal();
+}
+#endif
+
+bool HTMLInputElement::IsInteractiveHTMLContent() const {
+ return mType != FormControlType::InputHidden ||
+ nsGenericHTMLFormControlElementWithState::IsInteractiveHTMLContent();
+}
+
+void HTMLInputElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
+ nsImageLoadingContent::AsyncEventRunning(aEvent);
+}
+
+void HTMLInputElement::Select() {
+ if (!IsSingleLineTextControl(false)) {
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "Single line text controls are expected to have a state");
+
+ if (FocusState() != FocusTristate::eUnfocusable) {
+ RefPtr<nsFrameSelection> fs = state->GetConstFrameSelection();
+ if (fs && fs->MouseDownRecorded()) {
+ // This means that we're being called while the frame selection has a
+ // mouse down event recorded to adjust the caret during the mouse up
+ // event. We are probably called from the focus event handler. We should
+ // override the delayed caret data in this case to ensure that this
+ // select() call takes effect.
+ fs->SetDelayedCaretData(nullptr);
+ }
+
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ fm->SetFocus(this, nsIFocusManager::FLAG_NOSCROLL);
+
+ // A focus event handler may change the type attribute, which will destroy
+ // the previous state object.
+ state = GetEditorState();
+ if (!state) {
+ return;
+ }
+ }
+ }
+
+ // Directly call TextControlState::SetSelectionRange because
+ // HTMLInputElement::SetSelectionRange only applies to fewer types
+ state->SetSelectionRange(0, UINT32_MAX, Optional<nsAString>(), IgnoreErrors(),
+ TextControlState::ScrollAfterSelection::No);
+}
+
+void HTMLInputElement::SelectAll(nsPresContext* aPresContext) {
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(true);
+
+ if (formControlFrame) {
+ formControlFrame->SetFormProperty(nsGkAtoms::select, u""_ns);
+ }
+}
+
+bool HTMLInputElement::NeedToInitializeEditorForEvent(
+ EventChainPreVisitor& aVisitor) const {
+ // We only need to initialize the editor for single line input controls
+ // because they are lazily initialized. We don't need to initialize the
+ // control for certain types of events, because we know that those events are
+ // safe to be handled without the editor being initialized. These events
+ // include: mousein/move/out, overflow/underflow, DOM mutation, and void
+ // events. Void events are dispatched frequently by async keyboard scrolling
+ // to focused elements, so it's important to handle them to prevent excessive
+ // DOM mutations.
+ if (!IsSingleLineTextControl(false) ||
+ aVisitor.mEvent->mClass == eMutationEventClass) {
+ return false;
+ }
+
+ 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;
+ }
+}
+
+bool HTMLInputElement::IsDisabledForEvents(WidgetEvent* aEvent) {
+ return IsElementDisabledForEvents(aEvent, GetPrimaryFrame());
+}
+
+bool HTMLInputElement::CheckActivationBehaviorPreconditions(
+ EventChainVisitor& aVisitor) const {
+ switch (mType) {
+ case FormControlType::InputColor:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRadio:
+ case FormControlType::InputFile:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ case FormControlType::InputReset:
+ case FormControlType::InputButton: {
+ // Track whether we're in the outermost Dispatch invocation that will
+ // cause activation of the input. That is, if we're a click event, or a
+ // DOMActivate that was dispatched directly, this will be set, but if
+ // we're a DOMActivate dispatched from click handling, it will not be set.
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ bool outerActivateEvent =
+ (mouseEvent && mouseEvent->IsLeftClickEvent()) ||
+ (aVisitor.mEvent->mMessage == eLegacyDOMActivate &&
+ !mInInternalActivate);
+ if (outerActivateEvent) {
+ aVisitor.mItemFlags |= NS_OUTER_ACTIVATE_EVENT;
+ }
+ return outerActivateEvent;
+ }
+ default:
+ return false;
+ }
+}
+
+void HTMLInputElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ // Do not process any DOM events if the element is disabled
+ aVisitor.mCanHandle = false;
+ if (IsDisabledForEvents(aVisitor.mEvent)) {
+ return;
+ }
+
+ // Initialize the editor if needed.
+ if (NeedToInitializeEditorForEvent(aVisitor)) {
+ nsITextControlFrame* textControlFrame = do_QueryFrame(GetPrimaryFrame());
+ if (textControlFrame) textControlFrame->EnsureEditorInitialized();
+ }
+
+ if (CheckActivationBehaviorPreconditions(aVisitor)) {
+ aVisitor.mWantsActivationBehavior = true;
+ }
+
+ // We must cache type because mType may change during JS event (bug 2369)
+ aVisitor.mItemFlags |= uint8_t(mType);
+
+ if (aVisitor.mEvent->mMessage == eFocus && aVisitor.mEvent->IsTrusted() &&
+ MayFireChangeOnBlur() &&
+ // StartRangeThumbDrag already set mFocusedValue on 'mousedown' before
+ // we get the 'focus' event.
+ !mIsDraggingRange) {
+ GetValue(mFocusedValue, CallerType::System);
+ }
+
+ // Fire onchange (if necessary), before we do the blur, bug 357684.
+ if (aVisitor.mEvent->mMessage == eBlur) {
+ // We set NS_PRE_HANDLE_BLUR_EVENT here and handle it in PreHandleEvent to
+ // prevent breaking event target chain creation.
+ aVisitor.mWantsPreHandleEvent = true;
+ aVisitor.mItemFlags |= NS_PRE_HANDLE_BLUR_EVENT;
+ }
+
+ if (mType == FormControlType::InputRange &&
+ (aVisitor.mEvent->mMessage == eFocus ||
+ aVisitor.mEvent->mMessage == eBlur)) {
+ // Just as nsGenericHTMLFormControlElementWithState::GetEventTargetParent
+ // calls nsIFormControlFrame::SetFocus, we handle focus here.
+ nsIFrame* frame = GetPrimaryFrame();
+ if (frame) {
+ frame->InvalidateFrameSubtree();
+ }
+ }
+
+ if (mType == FormControlType::InputNumber && aVisitor.mEvent->IsTrusted()) {
+ if (mNumberControlSpinnerIsSpinning) {
+ // If the timer is running the user has depressed the mouse on one of the
+ // spin buttons. If the mouse exits the button we either want to reverse
+ // the direction of spin if it has moved over the other button, or else
+ // we want to end the spin. We do this here (rather than in
+ // PostHandleEvent) because we don't want to let content preventDefault()
+ // the end of the spin.
+ if (aVisitor.mEvent->mMessage == eMouseMove) {
+ // Be aggressive about stopping the spin:
+ bool stopSpin = true;
+ nsNumberControlFrame* numberControlFrame =
+ do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ bool oldNumberControlSpinTimerSpinsUpValue =
+ mNumberControlSpinnerSpinsUp;
+ switch (numberControlFrame->GetSpinButtonForPointerEvent(
+ aVisitor.mEvent->AsMouseEvent())) {
+ case nsNumberControlFrame::eSpinButtonUp:
+ mNumberControlSpinnerSpinsUp = true;
+ stopSpin = false;
+ break;
+ case nsNumberControlFrame::eSpinButtonDown:
+ mNumberControlSpinnerSpinsUp = false;
+ stopSpin = false;
+ break;
+ }
+ if (mNumberControlSpinnerSpinsUp !=
+ oldNumberControlSpinTimerSpinsUpValue) {
+ nsNumberControlFrame* numberControlFrame =
+ do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ numberControlFrame->SpinnerStateChanged();
+ }
+ }
+ }
+ if (stopSpin) {
+ StopNumberControlSpinnerSpin();
+ }
+ } else if (aVisitor.mEvent->mMessage == eMouseUp) {
+ StopNumberControlSpinnerSpin();
+ }
+ }
+ }
+
+ nsGenericHTMLFormControlElementWithState::GetEventTargetParent(aVisitor);
+
+ // Stop the event if the related target's first non-native ancestor is the
+ // same as the original target's first non-native ancestor (we are moving
+ // inside of the same element).
+ //
+ // FIXME(emilio): Is this still needed now that we use Shadow DOM for this?
+ if (CreatesDateTimeWidget() && aVisitor.mEvent->IsTrusted() &&
+ (aVisitor.mEvent->mMessage == eFocus ||
+ aVisitor.mEvent->mMessage == eFocusIn ||
+ aVisitor.mEvent->mMessage == eFocusOut ||
+ aVisitor.mEvent->mMessage == eBlur)) {
+ nsIContent* originalTarget = nsIContent::FromEventTargetOrNull(
+ aVisitor.mEvent->AsFocusEvent()->mOriginalTarget);
+ nsIContent* relatedTarget = nsIContent::FromEventTargetOrNull(
+ aVisitor.mEvent->AsFocusEvent()->mRelatedTarget);
+
+ if (originalTarget && relatedTarget &&
+ originalTarget->FindFirstNonChromeOnlyAccessContent() ==
+ relatedTarget->FindFirstNonChromeOnlyAccessContent()) {
+ aVisitor.mCanHandle = false;
+ }
+ }
+}
+
+void HTMLInputElement::LegacyPreActivationBehavior(
+ EventChainVisitor& aVisitor) {
+ //
+ // Web pages expect the value of a radio button or checkbox to be set
+ // *before* onclick and DOMActivate fire, and they expect that if they set
+ // the value explicitly during onclick or DOMActivate it will not be toggled
+ // or any such nonsense.
+ // In order to support that (bug 57137 and 58460 are examples) we toggle
+ // the checked attribute *first*, and then fire onclick. If the user
+ // returns false, we reset the control to the old checked value. Otherwise,
+ // we dispatch DOMActivate. If DOMActivate is cancelled, we also reset
+ // the control to the old checked value. We need to keep track of whether
+ // we've already toggled the state from onclick since the user could
+ // explicitly dispatch DOMActivate on the element.
+ //
+ // These are compatibility hacks and are defined as legacy-pre-activation
+ // and legacy-canceled-activation behavior in HTML.
+ //
+
+ // Assert mType didn't change after GetEventTargetParent
+ MOZ_ASSERT(NS_CONTROL_TYPE(aVisitor.mItemFlags) == uint8_t(mType));
+
+ bool originalCheckedValue = false;
+ mCheckedIsToggled = false;
+
+ if (mType == FormControlType::InputCheckbox) {
+ if (mIndeterminate) {
+ // indeterminate is always set to FALSE when the checkbox is toggled
+ SetIndeterminateInternal(false, false);
+ aVisitor.mItemFlags |= NS_ORIGINAL_INDETERMINATE_VALUE;
+ }
+
+ originalCheckedValue = Checked();
+ DoSetChecked(!originalCheckedValue, true, true);
+ mCheckedIsToggled = true;
+
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) {
+ aVisitor.mEventStatus = nsEventStatus_eConsumeDoDefault;
+ }
+ } else if (mType == FormControlType::InputRadio) {
+ HTMLInputElement* selectedRadioButton = GetSelectedRadioButton();
+ aVisitor.mItemData = static_cast<Element*>(selectedRadioButton);
+
+ originalCheckedValue = Checked();
+ if (!originalCheckedValue) {
+ DoSetChecked(true, true, true);
+ mCheckedIsToggled = true;
+ }
+
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) {
+ aVisitor.mEventStatus = nsEventStatus_eConsumeDoDefault;
+ }
+ }
+
+ if (originalCheckedValue) {
+ aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE;
+ }
+
+ // out-of-spec legacy pre-activation behavior needed because of bug 1803805
+ if ((mType == FormControlType::InputSubmit ||
+ mType == FormControlType::InputImage) &&
+ mForm) {
+ aVisitor.mItemFlags |= NS_IN_SUBMIT_CLICK;
+ aVisitor.mItemData = static_cast<Element*>(mForm);
+ // tell the form that we are about to enter a click handler.
+ // that means that if there are scripted submissions, the
+ // latest one will be deferred until after the exit point of the
+ // handler.
+ mForm->OnSubmitClickBegin(this);
+ }
+}
+
+nsresult HTMLInputElement::PreHandleEvent(EventChainVisitor& aVisitor) {
+ if (aVisitor.mItemFlags & NS_PRE_HANDLE_BLUR_EVENT) {
+ MOZ_ASSERT(aVisitor.mEvent->mMessage == eBlur);
+ FireChangeEventIfNeeded();
+ }
+ return nsGenericHTMLFormControlElementWithState::PreHandleEvent(aVisitor);
+}
+
+void HTMLInputElement::StartRangeThumbDrag(WidgetGUIEvent* aEvent) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame());
+ if (!rangeFrame) {
+ return;
+ }
+
+ mIsDraggingRange = true;
+ mRangeThumbDragStartValue = GetValueAsDecimal();
+ // Don't use CaptureFlags::RetargetToElement, as that breaks pseudo-class
+ // styling of the thumb.
+ PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState);
+
+ // Before we change the value, record the current value so that we'll
+ // correctly send a 'change' event if appropriate. We need to do this here
+ // because the 'focus' event is handled after the 'mousedown' event that
+ // we're being called for (i.e. too late to update mFocusedValue, since we'll
+ // have changed it by then).
+ GetValue(mFocusedValue, CallerType::System);
+
+ SetValueOfRangeForUserEvent(rangeFrame->GetValueAtEventPoint(aEvent),
+ SnapToTickMarks::Yes);
+}
+
+void HTMLInputElement::FinishRangeThumbDrag(WidgetGUIEvent* aEvent) {
+ MOZ_ASSERT(mIsDraggingRange);
+
+ if (PresShell::GetCapturingContent() == this) {
+ PresShell::ReleaseCapturingContent();
+ }
+ if (aEvent) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame());
+ SetValueOfRangeForUserEvent(rangeFrame->GetValueAtEventPoint(aEvent),
+ SnapToTickMarks::Yes);
+ }
+ mIsDraggingRange = false;
+ FireChangeEventIfNeeded();
+}
+
+void HTMLInputElement::CancelRangeThumbDrag(bool aIsForUserEvent) {
+ MOZ_ASSERT(mIsDraggingRange);
+
+ mIsDraggingRange = false;
+ if (PresShell::GetCapturingContent() == this) {
+ PresShell::ReleaseCapturingContent();
+ }
+ if (aIsForUserEvent) {
+ SetValueOfRangeForUserEvent(mRangeThumbDragStartValue,
+ SnapToTickMarks::Yes);
+ } else {
+ // Don't dispatch an 'input' event - at least not using
+ // DispatchTrustedEvent.
+ // TODO: decide what we should do here - bug 851782.
+ nsAutoString val;
+ mInputType->ConvertNumberToString(mRangeThumbDragStartValue, val);
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // is small, so we should be fine here.)
+ SetValueInternal(val, {ValueSetterOption::BySetUserInputAPI,
+ ValueSetterOption::SetValueChanged});
+ if (nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame())) {
+ frame->UpdateForValueChange();
+ }
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ }
+}
+
+void HTMLInputElement::SetValueOfRangeForUserEvent(
+ Decimal aValue, SnapToTickMarks aSnapToTickMarks) {
+ MOZ_ASSERT(aValue.isFinite());
+ if (aSnapToTickMarks == SnapToTickMarks::Yes) {
+ MaybeSnapToTickMark(aValue);
+ }
+
+ Decimal oldValue = GetValueAsDecimal();
+
+ nsAutoString val;
+ mInputType->ConvertNumberToString(aValue, val);
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // is small, so we should be fine here.)
+ SetValueInternal(val, {ValueSetterOption::BySetUserInputAPI,
+ ValueSetterOption::SetValueChanged});
+ if (nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame())) {
+ frame->UpdateForValueChange();
+ }
+
+ if (GetValueAsDecimal() != oldValue) {
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ }
+}
+
+void HTMLInputElement::StartNumberControlSpinnerSpin() {
+ MOZ_ASSERT(!mNumberControlSpinnerIsSpinning);
+
+ mNumberControlSpinnerIsSpinning = true;
+
+ nsRepeatService::GetInstance()->Start(
+ HandleNumberControlSpin, this, OwnerDoc(), "HandleNumberControlSpin"_ns);
+
+ // Capture the mouse so that we can tell if the pointer moves from one
+ // spin button to the other, or to some other element:
+ PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState);
+
+ nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ numberControlFrame->SpinnerStateChanged();
+ }
+}
+
+void HTMLInputElement::StopNumberControlSpinnerSpin(SpinnerStopState aState) {
+ if (mNumberControlSpinnerIsSpinning) {
+ if (PresShell::GetCapturingContent() == this) {
+ PresShell::ReleaseCapturingContent();
+ }
+
+ nsRepeatService::GetInstance()->Stop(HandleNumberControlSpin, this);
+
+ mNumberControlSpinnerIsSpinning = false;
+
+ if (aState == eAllowDispatchingEvents) {
+ FireChangeEventIfNeeded();
+ }
+
+ nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ MOZ_ASSERT(aState == eAllowDispatchingEvents,
+ "Shouldn't have primary frame for the element when we're not "
+ "allowed to dispatch events to it anymore.");
+ numberControlFrame->SpinnerStateChanged();
+ }
+ }
+}
+
+void HTMLInputElement::StepNumberControlForUserEvent(int32_t aDirection) {
+ // We can't use GetValidityState here because the validity state is not set
+ // if the user hasn't previously taken an action to set or change the value,
+ // according to the specs.
+ if (HasBadInput()) {
+ // If the user has typed a value into the control and inadvertently made a
+ // mistake (e.g. put a thousand separator at the wrong point) we do not
+ // want to wipe out what they typed if they try to increment/decrement the
+ // value. Better is to highlight the value as being invalid so that they
+ // can correct what they typed.
+ // We only do this if there actually is a value typed in by/displayed to
+ // the user. (IsValid() can return false if the 'required' attribute is
+ // set and the value is the empty string.)
+ if (!IsValueEmpty()) {
+ // We pass 'true' for SetUserInteracted because we need the UI to update
+ // _now_ or the user will wonder why the step behavior isn't functioning.
+ SetUserInteracted(true);
+ return;
+ }
+ }
+
+ Decimal newValue = Decimal::nan(); // unchanged if value will not change
+
+ nsresult rv = GetValueIfStepped(aDirection, CALLED_FOR_USER_EVENT, &newValue);
+
+ if (NS_FAILED(rv) || !newValue.isFinite()) {
+ return; // value should not or will not change
+ }
+
+ nsAutoString newVal;
+ mInputType->ConvertNumberToString(newValue, newVal);
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // is small, so we should be fine here.)
+ SetValueInternal(newVal, {ValueSetterOption::BySetUserInputAPI,
+ ValueSetterOption::SetValueChanged});
+}
+
+static bool SelectTextFieldOnFocus() {
+ if (!gSelectTextFieldOnFocus) {
+ int32_t selectTextfieldsOnKeyFocus = -1;
+ nsresult rv =
+ LookAndFeel::GetInt(LookAndFeel::IntID::SelectTextfieldsOnKeyFocus,
+ &selectTextfieldsOnKeyFocus);
+ if (NS_FAILED(rv)) {
+ gSelectTextFieldOnFocus = -1;
+ } else {
+ gSelectTextFieldOnFocus = selectTextfieldsOnKeyFocus != 0 ? 1 : -1;
+ }
+ }
+
+ return gSelectTextFieldOnFocus == 1;
+}
+
+bool HTMLInputElement::ShouldPreventDOMActivateDispatch(
+ EventTarget* aOriginalTarget) {
+ /*
+ * For the moment, there is only one situation where we actually want to
+ * prevent firing a DOMActivate event:
+ * - we are a <input type='file'> that just got a click event,
+ * - the event was targeted to our button which should have sent a
+ * DOMActivate event.
+ */
+
+ if (mType != FormControlType::InputFile) {
+ return false;
+ }
+
+ Element* target = Element::FromEventTargetOrNull(aOriginalTarget);
+ if (!target) {
+ return false;
+ }
+
+ return target->GetParent() == this &&
+ target->IsRootOfNativeAnonymousSubtree() &&
+ target->IsHTMLElement(nsGkAtoms::button);
+}
+
+nsresult HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor) {
+ // Open a file picker when we receive a click on a <input type='file'>, or
+ // open a color picker when we receive a click on a <input type='color'>.
+ // A click is handled if it's the left mouse button.
+ // We do not prevent non-trusted click because authors can already use
+ // .click(). However, the pickers will follow the rules of popup-blocking.
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (!(mouseEvent && mouseEvent->IsLeftClickEvent())) {
+ return NS_OK;
+ }
+ if (mType == FormControlType::InputFile) {
+ // If the user clicked on the "Choose folder..." button we open the
+ // directory picker, else we open the file picker.
+ FilePickerType type = FILE_PICKER_FILE;
+ nsIContent* target =
+ nsIContent::FromEventTargetOrNull(aVisitor.mEvent->mOriginalTarget);
+ if (target && target->FindFirstNonChromeOnlyAccessContent() == this &&
+ StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ HasAttr(nsGkAtoms::webkitdirectory)) {
+ type = FILE_PICKER_DIRECTORY;
+ }
+ return InitFilePicker(type);
+ }
+ if (mType == FormControlType::InputColor) {
+ return InitColorPicker();
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Return true if the input event should be ignored because of its modifiers.
+ * Control is treated specially, since sometimes we ignore it, and sometimes
+ * we don't (for webcompat reasons).
+ */
+static bool IgnoreInputEventWithModifier(const WidgetInputEvent& aEvent,
+ bool ignoreControl) {
+ return (ignoreControl && aEvent.IsControl()) ||
+ aEvent.IsAltGraph()
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+ // Meta key is the Windows Logo key on Windows and Linux which may
+ // assign some special meaning for the events while it's pressed.
+ // On the other hand, it's a normal modifier in macOS and Android.
+ // Therefore, We should ignore it only in Win/Linux.
+ || aEvent.IsMeta()
+#endif
+ || aEvent.IsFn();
+}
+
+bool HTMLInputElement::StepsInputValue(
+ const WidgetKeyboardEvent& aEvent) const {
+ if (mType != FormControlType::InputNumber) {
+ return false;
+ }
+ if (aEvent.mMessage != eKeyPress) {
+ return false;
+ }
+ if (!aEvent.IsTrusted()) {
+ return false;
+ }
+ if (aEvent.mKeyCode != NS_VK_UP && aEvent.mKeyCode != NS_VK_DOWN) {
+ return false;
+ }
+ if (IgnoreInputEventWithModifier(aEvent, false)) {
+ return false;
+ }
+ if (aEvent.DefaultPrevented()) {
+ return false;
+ }
+ if (!IsMutable()) {
+ return false;
+ }
+ return true;
+}
+
+static bool ActivatesWithKeyboard(FormControlType aType, uint32_t aKeyCode) {
+ switch (aType) {
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRadio:
+ // Checkbox and Radio try to submit on Enter press
+ return aKeyCode != NS_VK_RETURN;
+ case FormControlType::InputButton:
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputFile:
+ case FormControlType::InputImage: // Bug 34418
+ case FormControlType::InputColor:
+ return true;
+ default:
+ return false;
+ }
+}
+
+nsresult HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ if (aVisitor.mEvent->mMessage == eBlur) {
+ if (mIsDraggingRange) {
+ FinishRangeThumbDrag();
+ } else if (mNumberControlSpinnerIsSpinning) {
+ StopNumberControlSpinnerSpin();
+ }
+ }
+
+ nsresult rv = NS_OK;
+ auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags));
+
+ // Ideally we would make the default action for click and space just dispatch
+ // DOMActivate, and the default action for DOMActivate flip the checkbox/
+ // radio state and fire onchange. However, for backwards compatibility, we
+ // need to flip the state before firing click, and we need to fire click
+ // when space is pressed. So, we just nest the firing of DOMActivate inside
+ // the click event handling, and allow cancellation of DOMActivate to cancel
+ // the click.
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault &&
+ !IsSingleLineTextControl(true) && mType != FormControlType::InputNumber) {
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (mouseEvent && mouseEvent->IsLeftClickEvent() &&
+ OwnerDoc()->MayHaveDOMActivateListeners() &&
+ !ShouldPreventDOMActivateDispatch(aVisitor.mEvent->mOriginalTarget)) {
+ // DOMActive event should be trusted since the activation is actually
+ // occurred even if the cause is an untrusted click event.
+ InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent);
+ actEvent.mDetail = 1;
+
+ if (RefPtr<PresShell> presShell =
+ aVisitor.mPresContext ? aVisitor.mPresContext->GetPresShell()
+ : nullptr) {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mInInternalActivate = true;
+ rv = presShell->HandleDOMEventWithTarget(this, &actEvent, &status);
+ mInInternalActivate = false;
+
+ // If activate is cancelled, we must do the same as when click is
+ // cancelled (revert the checkbox to its original value).
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ aVisitor.mEventStatus = status;
+ }
+ }
+ }
+ }
+
+ bool preventDefault =
+ aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault;
+ if (IsDisabled() && oldType != FormControlType::InputCheckbox &&
+ oldType != FormControlType::InputRadio) {
+ // Behave as if defaultPrevented when the element becomes disabled by event
+ // listeners. Checkboxes and radio buttons should still process clicks for
+ // web compat. See:
+ // https://html.spec.whatwg.org/multipage/input.html#the-input-element:activation-behaviour
+ preventDefault = true;
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent();
+ if (keyEvent && StepsInputValue(*keyEvent)) {
+ StepNumberControlForUserEvent(keyEvent->mKeyCode == NS_VK_UP ? 1 : -1);
+ FireChangeEventIfNeeded();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ } else if (!preventDefault) {
+ if (keyEvent && ActivatesWithKeyboard(mType, keyEvent->mKeyCode) &&
+ keyEvent->IsTrusted()) {
+ // We maybe dispatch a synthesized click for keyboard activation.
+ HandleKeyboardActivation(aVisitor);
+ }
+
+ switch (aVisitor.mEvent->mMessage) {
+ case eFocus: {
+ // 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.
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm && IsSingleLineTextControl(false) &&
+ !aVisitor.mEvent->AsFocusEvent()->mFromRaise &&
+ SelectTextFieldOnFocus()) {
+ if (Document* document = GetComposedDoc()) {
+ uint32_t lastFocusMethod =
+ fm->GetLastFocusMethod(document->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) {
+ RefPtr<nsPresContext> presContext =
+ GetPresContext(eForComposedDoc);
+ SelectAll(presContext);
+ }
+ }
+ }
+ break;
+ }
+
+ case eKeyDown: {
+ // For compatibility with the other browsers, we should active this
+ // element at least when a checkbox or a radio button.
+ // TODO: Investigate which elements are activated by space key in the
+ // other browsers.
+ if (aVisitor.mPresContext && keyEvent->IsTrusted() && !IsDisabled() &&
+ keyEvent->ShouldWorkAsSpaceKey() &&
+ (mType == FormControlType::InputCheckbox ||
+ mType == FormControlType::InputRadio)) {
+ EventStateManager::SetActiveManager(
+ aVisitor.mPresContext->EventStateManager(), this);
+ }
+ break;
+ }
+
+ case eKeyPress: {
+ if (mType == FormControlType::InputRadio && keyEvent->IsTrusted() &&
+ !keyEvent->IsAlt() && !keyEvent->IsControl() &&
+ !keyEvent->IsMeta()) {
+ rv = MaybeHandleRadioButtonNavigation(aVisitor, keyEvent->mKeyCode);
+ }
+
+ /*
+ * For some input types, if the user hits enter, the form is
+ * submitted.
+ *
+ * Bug 99920, bug 109463 and bug 147850:
+ * (a) if there is a submit control in the form, click the first
+ * submit control in the form.
+ * (b) if there is just one text control in the form, submit by
+ * sending a submit event directly to the form
+ * (c) if there is more than one text input and no submit buttons, do
+ * not submit, period.
+ */
+
+ if (keyEvent->mKeyCode == NS_VK_RETURN && keyEvent->IsTrusted() &&
+ (IsSingleLineTextControl(false, mType) ||
+ IsDateTimeInputType(mType) ||
+ mType == FormControlType::InputCheckbox ||
+ mType == FormControlType::InputRadio)) {
+ if (IsSingleLineTextControl(false, mType) ||
+ IsDateTimeInputType(mType)) {
+ FireChangeEventIfNeeded();
+ }
+
+ if (aVisitor.mPresContext) {
+ MaybeSubmitForm(aVisitor.mPresContext);
+ }
+ }
+
+ if (mType == FormControlType::InputRange && keyEvent->IsTrusted() &&
+ !keyEvent->IsAlt() && !keyEvent->IsControl() &&
+ !keyEvent->IsMeta() &&
+ (keyEvent->mKeyCode == NS_VK_LEFT ||
+ keyEvent->mKeyCode == NS_VK_RIGHT ||
+ keyEvent->mKeyCode == NS_VK_UP ||
+ keyEvent->mKeyCode == NS_VK_DOWN ||
+ keyEvent->mKeyCode == NS_VK_PAGE_UP ||
+ keyEvent->mKeyCode == NS_VK_PAGE_DOWN ||
+ keyEvent->mKeyCode == NS_VK_HOME ||
+ keyEvent->mKeyCode == NS_VK_END)) {
+ Decimal minimum = GetMinimum();
+ Decimal maximum = GetMaximum();
+ MOZ_ASSERT(minimum.isFinite() && maximum.isFinite());
+ if (minimum < maximum) { // else the value is locked to the minimum
+ Decimal value = GetValueAsDecimal();
+ Decimal step = GetStep();
+ if (step == kStepAny) {
+ step = GetDefaultStep();
+ }
+ MOZ_ASSERT(value.isFinite() && step.isFinite());
+ Decimal newValue;
+ switch (keyEvent->mKeyCode) {
+ case NS_VK_LEFT:
+ newValue = value +
+ (GetComputedDirectionality() == Directionality::Rtl
+ ? step
+ : -step);
+ break;
+ case NS_VK_RIGHT:
+ newValue = value +
+ (GetComputedDirectionality() == Directionality::Rtl
+ ? -step
+ : step);
+ break;
+ case NS_VK_UP:
+ // Even for horizontal range, "up" means "increase"
+ newValue = value + step;
+ break;
+ case NS_VK_DOWN:
+ // Even for horizontal range, "down" means "decrease"
+ newValue = value - step;
+ break;
+ case NS_VK_HOME:
+ newValue = minimum;
+ break;
+ case NS_VK_END:
+ newValue = maximum;
+ break;
+ case NS_VK_PAGE_UP:
+ // For PgUp/PgDn we jump 10% of the total range, unless step
+ // requires us to jump more.
+ newValue =
+ value + std::max(step, (maximum - minimum) / Decimal(10));
+ break;
+ case NS_VK_PAGE_DOWN:
+ newValue =
+ value - std::max(step, (maximum - minimum) / Decimal(10));
+ break;
+ }
+ SetValueOfRangeForUserEvent(newValue);
+ FireChangeEventIfNeeded();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ }
+
+ } break; // eKeyPress
+
+ case eMouseDown:
+ case eMouseUp:
+ case eMouseDoubleClick: {
+ // cancel all of these events for buttons
+ // XXXsmaug Why?
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (mouseEvent->mButton == MouseButton::eMiddle ||
+ mouseEvent->mButton == MouseButton::eSecondary) {
+ if (mType == FormControlType::InputButton ||
+ mType == FormControlType::InputReset ||
+ mType == FormControlType::InputSubmit) {
+ if (aVisitor.mDOMEvent) {
+ aVisitor.mDOMEvent->StopPropagation();
+ } else {
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+ }
+ if (mType == FormControlType::InputNumber &&
+ aVisitor.mEvent->IsTrusted()) {
+ if (mouseEvent->mButton == MouseButton::ePrimary &&
+ !IgnoreInputEventWithModifier(*mouseEvent, false)) {
+ nsNumberControlFrame* numberControlFrame =
+ do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ if (aVisitor.mEvent->mMessage == eMouseDown && IsMutable()) {
+ switch (numberControlFrame->GetSpinButtonForPointerEvent(
+ aVisitor.mEvent->AsMouseEvent())) {
+ case nsNumberControlFrame::eSpinButtonUp:
+ StepNumberControlForUserEvent(1);
+ mNumberControlSpinnerSpinsUp = true;
+ StartNumberControlSpinnerSpin();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ break;
+ case nsNumberControlFrame::eSpinButtonDown:
+ StepNumberControlForUserEvent(-1);
+ mNumberControlSpinnerSpinsUp = false;
+ StartNumberControlSpinnerSpin();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ break;
+ }
+ }
+ }
+ }
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) {
+ // We didn't handle this to step up/down. Whatever this was, be
+ // aggressive about stopping the spin. (And don't set
+ // nsEventStatus_eConsumeNoDefault after doing so, since that
+ // might prevent, say, the context menu from opening.)
+ StopNumberControlSpinnerSpin();
+ }
+ }
+ break;
+ }
+#if !defined(ANDROID) && !defined(XP_MACOSX)
+ case eWheel: {
+ // Handle wheel events as increasing / decreasing the input element's
+ // value when it's focused and it's type is number or range.
+ WidgetWheelEvent* wheelEvent = aVisitor.mEvent->AsWheelEvent();
+ if (!aVisitor.mEvent->DefaultPrevented() &&
+ aVisitor.mEvent->IsTrusted() && IsMutable() && wheelEvent &&
+ wheelEvent->mDeltaY != 0 &&
+ wheelEvent->mDeltaMode != WheelEvent_Binding::DOM_DELTA_PIXEL) {
+ if (mType == FormControlType::InputNumber) {
+ if (nsContentUtils::IsFocusedContent(this)) {
+ StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1);
+ FireChangeEventIfNeeded();
+ aVisitor.mEvent->PreventDefault();
+ }
+ } else if (mType == FormControlType::InputRange &&
+ nsContentUtils::IsFocusedContent(this) &&
+ GetMinimum() < GetMaximum()) {
+ Decimal value = GetValueAsDecimal();
+ Decimal step = GetStep();
+ if (step == kStepAny) {
+ step = GetDefaultStep();
+ }
+ MOZ_ASSERT(value.isFinite() && step.isFinite());
+ SetValueOfRangeForUserEvent(
+ wheelEvent->mDeltaY < 0 ? value + step : value - step);
+ FireChangeEventIfNeeded();
+ aVisitor.mEvent->PreventDefault();
+ }
+ }
+ break;
+ }
+#endif
+ case eMouseClick: {
+ if (!aVisitor.mEvent->DefaultPrevented() &&
+ aVisitor.mEvent->IsTrusted() &&
+ aVisitor.mEvent->AsMouseEvent()->mButton ==
+ MouseButton::ePrimary) {
+ // TODO(emilio): Handling this should ideally not move focus.
+ if (mType == FormControlType::InputSearch) {
+ if (nsSearchControlFrame* searchControlFrame =
+ do_QueryFrame(GetPrimaryFrame())) {
+ Element* clearButton = searchControlFrame->GetAnonClearButton();
+ if (clearButton &&
+ aVisitor.mEvent->mOriginalTarget == clearButton) {
+ SetUserInput(EmptyString(),
+ *nsContentUtils::GetSystemPrincipal());
+ // TODO(emilio): This should focus the input, but calling
+ // SetFocus(this, FLAG_NOSCROLL) for some reason gets us into
+ // an inconsistent state where we're focused but don't match
+ // :focus-visible / :focus.
+ }
+ }
+ } else if (mType == FormControlType::InputPassword) {
+ if (nsTextControlFrame* textControlFrame =
+ do_QueryFrame(GetPrimaryFrame())) {
+ auto* reveal = textControlFrame->GetRevealButton();
+ if (reveal && aVisitor.mEvent->mOriginalTarget == reveal) {
+ SetRevealPassword(!RevealPassword());
+ // TODO(emilio): This should focus the input, but calling
+ // SetFocus(this, FLAG_NOSCROLL) for some reason gets us into
+ // an inconsistent state where we're focused but don't match
+ // :focus-visible / :focus.
+ }
+ }
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ // Bug 1459231: Temporarily needed till links respect activation target,
+ // then also remove NS_OUTER_ACTIVATE_EVENT. The appropriate
+ // behavior/model for links is still under discussion (see
+ // https://github.com/whatwg/html/issues/1576). For now, we aim for
+ // consistency with other browsers.
+ if (aVisitor.mItemFlags & NS_OUTER_ACTIVATE_EVENT) {
+ switch (mType) {
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ if (mForm) {
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ }
+ break;
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRadio:
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ } // if
+
+ if (NS_SUCCEEDED(rv) && mType == FormControlType::InputRange) {
+ PostHandleEventForRangeThumb(aVisitor);
+ }
+
+ if (!preventDefault) {
+ MOZ_TRY(MaybeInitPickers(aVisitor));
+ }
+ return NS_OK;
+}
+
+void EndSubmitClick(EventChainPostVisitor& aVisitor) {
+ auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags));
+ if ((aVisitor.mItemFlags & NS_IN_SUBMIT_CLICK) &&
+ (oldType == FormControlType::InputSubmit ||
+ oldType == FormControlType::InputImage)) {
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mItemData));
+ RefPtr<HTMLFormElement> form = HTMLFormElement::FromNodeOrNull(content);
+ // Tell the form that we are about to exit a click handler,
+ // so the form knows not to defer subsequent submissions.
+ // The pending ones that were created during the handler
+ // will be flushed or forgotten.
+ form->OnSubmitClickEnd();
+ // tell the form to flush a possible pending submission.
+ // the reason is that the script returned false (the event was
+ // not ignored) so if there is a stored submission, it needs to
+ // be submitted immediately.
+ form->FlushPendingSubmission();
+ }
+}
+
+void HTMLInputElement::ActivationBehavior(EventChainPostVisitor& aVisitor) {
+ auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags));
+
+ if (IsDisabled() && oldType != FormControlType::InputCheckbox &&
+ oldType != FormControlType::InputRadio) {
+ // Behave as if defaultPrevented when the element becomes disabled by event
+ // listeners. Checkboxes and radio buttons should still process clicks for
+ // web compat. See:
+ // https://html.spec.whatwg.org/multipage/input.html#the-input-element:activation-behaviour
+ EndSubmitClick(aVisitor);
+ return;
+ }
+
+ if (mCheckedIsToggled) {
+ SetUserInteracted(true);
+
+ // Fire input event and then change event.
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+
+ // FIXME: Why is this different than every other change event?
+ nsContentUtils::DispatchTrustedEvent<WidgetEvent>(
+ OwnerDoc(), static_cast<Element*>(this), eFormChange, CanBubble::eYes,
+ Cancelable::eNo);
+#ifdef ACCESSIBILITY
+ // Fire an event to notify accessibility
+ if (mType == FormControlType::InputCheckbox) {
+ if (nsContentUtils::MayHaveFormCheckboxStateChangeListeners()) {
+ FireEventForAccessibility(this, eFormCheckboxStateChange);
+ }
+ } else if (nsContentUtils::MayHaveFormRadioStateChangeListeners()) {
+ FireEventForAccessibility(this, eFormRadioStateChange);
+ // Fire event for the previous selected radio.
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aVisitor.mItemData);
+ if (auto* previous = HTMLInputElement::FromNodeOrNull(content)) {
+ FireEventForAccessibility(previous, eFormRadioStateChange);
+ }
+ }
+#endif
+ }
+
+ switch (mType) {
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ if (mForm) {
+ // Hold a strong ref while dispatching
+ RefPtr<HTMLFormElement> form(mForm);
+ if (mType == FormControlType::InputReset) {
+ form->MaybeReset(this);
+ } else {
+ form->MaybeSubmit(this);
+ }
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ break;
+
+ default:
+ break;
+ } // switch
+ if (IsButtonControl()) {
+ if (!GetInvokeTargetElement()) {
+ HandlePopoverTargetAction();
+ } else {
+ HandleInvokeTargetAction();
+ }
+ }
+
+ EndSubmitClick(aVisitor);
+}
+
+void HTMLInputElement::LegacyCanceledActivationBehavior(
+ EventChainPostVisitor& aVisitor) {
+ bool originalCheckedValue =
+ !!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE);
+ auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags));
+
+ if (mCheckedIsToggled) {
+ // if it was canceled and a radio button, then set the old
+ // selected btn to TRUE. if it is a checkbox then set it to its
+ // original value (legacy-canceled-activation)
+ if (oldType == FormControlType::InputRadio) {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aVisitor.mItemData);
+ HTMLInputElement* selectedRadioButton =
+ HTMLInputElement::FromNodeOrNull(content);
+ if (selectedRadioButton) {
+ selectedRadioButton->SetChecked(true);
+ }
+ // If there was no checked radio button or this one is no longer a
+ // radio button we must reset it back to false to cancel the action.
+ // See how the web of hack grows?
+ if (!selectedRadioButton || mType != FormControlType::InputRadio) {
+ DoSetChecked(false, true, true);
+ }
+ } else if (oldType == FormControlType::InputCheckbox) {
+ bool originalIndeterminateValue =
+ !!(aVisitor.mItemFlags & NS_ORIGINAL_INDETERMINATE_VALUE);
+ SetIndeterminateInternal(originalIndeterminateValue, false);
+ DoSetChecked(originalCheckedValue, true, true);
+ }
+ }
+
+ // Relevant for bug 242494: submit button with "submit(); return false;"
+ EndSubmitClick(aVisitor);
+}
+
+enum class RadioButtonMove { Back, Forward, None };
+nsresult HTMLInputElement::MaybeHandleRadioButtonNavigation(
+ EventChainPostVisitor& aVisitor, uint32_t aKeyCode) {
+ auto move = [&] {
+ switch (aKeyCode) {
+ case NS_VK_UP:
+ return RadioButtonMove::Back;
+ case NS_VK_DOWN:
+ return RadioButtonMove::Forward;
+ case NS_VK_LEFT:
+ case NS_VK_RIGHT: {
+ const bool isRtl = GetComputedDirectionality() == Directionality::Rtl;
+ return isRtl == (aKeyCode == NS_VK_LEFT) ? RadioButtonMove::Forward
+ : RadioButtonMove::Back;
+ }
+ }
+ return RadioButtonMove::None;
+ }();
+ if (move == RadioButtonMove::None) {
+ return NS_OK;
+ }
+ // Arrow key pressed, focus+select prev/next radio button
+ RefPtr<HTMLInputElement> selectedRadioButton;
+ if (auto* container = GetCurrentRadioGroupContainer()) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ container->GetNextRadioButton(name, move == RadioButtonMove::Back, this,
+ getter_AddRefs(selectedRadioButton));
+ }
+ if (!selectedRadioButton) {
+ return NS_OK;
+ }
+ FocusOptions options;
+ ErrorResult error;
+ selectedRadioButton->Focus(options, CallerType::System, error);
+ if (error.Failed()) {
+ return error.StealNSResult();
+ }
+ nsresult rv = DispatchSimulatedClick(
+ selectedRadioButton, aVisitor.mEvent->IsTrusted(), aVisitor.mPresContext);
+ if (NS_SUCCEEDED(rv)) {
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ return rv;
+}
+
+void HTMLInputElement::PostHandleEventForRangeThumb(
+ EventChainPostVisitor& aVisitor) {
+ MOZ_ASSERT(mType == FormControlType::InputRange);
+
+ if (nsEventStatus_eConsumeNoDefault == aVisitor.mEventStatus ||
+ !(aVisitor.mEvent->mClass == eMouseEventClass ||
+ aVisitor.mEvent->mClass == eTouchEventClass ||
+ aVisitor.mEvent->mClass == eKeyboardEventClass)) {
+ return;
+ }
+
+ nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame());
+ if (!rangeFrame && mIsDraggingRange) {
+ CancelRangeThumbDrag();
+ return;
+ }
+
+ switch (aVisitor.mEvent->mMessage) {
+ case eMouseDown:
+ case eTouchStart: {
+ if (mIsDraggingRange) {
+ break;
+ }
+ if (PresShell::GetCapturingContent()) {
+ break; // don't start drag if someone else is already capturing
+ }
+ WidgetInputEvent* inputEvent = aVisitor.mEvent->AsInputEvent();
+ if (IgnoreInputEventWithModifier(*inputEvent, true)) {
+ break; // ignore
+ }
+ if (aVisitor.mEvent->mMessage == eMouseDown) {
+ if (aVisitor.mEvent->AsMouseEvent()->mButtons ==
+ MouseButtonsFlag::ePrimaryFlag) {
+ StartRangeThumbDrag(inputEvent);
+ } else if (mIsDraggingRange) {
+ CancelRangeThumbDrag();
+ }
+ } else {
+ if (aVisitor.mEvent->AsTouchEvent()->mTouches.Length() == 1) {
+ StartRangeThumbDrag(inputEvent);
+ } else if (mIsDraggingRange) {
+ CancelRangeThumbDrag();
+ }
+ }
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ } break;
+
+ case eMouseMove:
+ case eTouchMove:
+ if (!mIsDraggingRange) {
+ break;
+ }
+ if (PresShell::GetCapturingContent() != this) {
+ // Someone else grabbed capture.
+ CancelRangeThumbDrag();
+ break;
+ }
+ SetValueOfRangeForUserEvent(
+ rangeFrame->GetValueAtEventPoint(aVisitor.mEvent->AsInputEvent()),
+ SnapToTickMarks::Yes);
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ break;
+
+ case eMouseUp:
+ case eTouchEnd:
+ if (!mIsDraggingRange) {
+ break;
+ }
+ // We don't check to see whether we are the capturing content here and
+ // call CancelRangeThumbDrag() if that is the case. We just finish off
+ // the drag and set our final value (unless someone has called
+ // preventDefault() and prevents us getting here).
+ FinishRangeThumbDrag(aVisitor.mEvent->AsInputEvent());
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ break;
+
+ case eKeyPress:
+ if (mIsDraggingRange &&
+ aVisitor.mEvent->AsKeyboardEvent()->mKeyCode == NS_VK_ESCAPE) {
+ CancelRangeThumbDrag();
+ }
+ break;
+
+ case eTouchCancel:
+ if (mIsDraggingRange) {
+ CancelRangeThumbDrag();
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+void HTMLInputElement::MaybeLoadImage() {
+ // Our base URI may have changed; claim that our URI changed, and the
+ // nsImageLoadingContent will decide whether a new image load is warranted.
+ nsAutoString uri;
+ if (mType == FormControlType::InputImage && GetAttr(nsGkAtoms::src, uri) &&
+ (NS_FAILED(LoadImage(uri, false, true, eImageLoadType_Normal,
+ mSrcTriggeringPrincipal)) ||
+ !LoadingEnabled())) {
+ CancelImageRequests(true);
+ }
+}
+
+nsresult HTMLInputElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ // If we are currently bound to a disconnected subtree root, remove
+ // ourselves from it first.
+ if (!mForm && mType == FormControlType::InputRadio) {
+ RemoveFromRadioGroup();
+ }
+
+ nsresult rv =
+ nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsImageLoadingContent::BindToTree(aContext, aParent);
+
+ if (mType == FormControlType::InputImage) {
+ // Our base URI may have changed; claim that our URI changed, and the
+ // nsImageLoadingContent will decide whether a new image load is warranted.
+ if (HasAttr(nsGkAtoms::src)) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initaiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod("dom::HTMLInputElement::MaybeLoadImage", this,
+ &HTMLInputElement::MaybeLoadImage));
+ }
+ }
+
+ // Add radio to document if we don't have a form already (if we do it's
+ // already been added into that group)
+ if (!mForm && mType == FormControlType::InputRadio) {
+ AddToRadioGroup();
+ }
+
+ // Set direction based on value if dir=auto
+ if (HasDirAuto()) {
+ SetAutoDirectionality(false);
+ }
+
+ // An element can't suffer from value missing if it is not in a document.
+ // We have to check if we suffer from that as we are now in a document.
+ UpdateValueMissingValidityState();
+
+ // If there is a disabled fieldset in the parent chain, the element is now
+ // barred from constraint validation and can't suffer from value missing
+ // (call done before).
+ UpdateBarredFromConstraintValidation();
+
+ // And now make sure our state is up to date
+ UpdateValidityElementStates(true);
+
+ if (CreatesDateTimeWidget() && IsInComposedDoc()) {
+ // Construct Shadow Root so web content can be hidden in the DOM.
+ AttachAndSetUAShadowRoot(NotifyUAWidgetSetup::Yes, DelegatesFocus::Yes);
+ }
+
+ MaybeDispatchLoginManagerEvents(mForm);
+
+ return rv;
+}
+
+void HTMLInputElement::MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm) {
+ // Don't disptach the event if the <input> is disconnected
+ // or belongs to a disconnected form
+ if (!IsInComposedDoc()) {
+ return;
+ }
+
+ nsString eventType;
+ Element* target = nullptr;
+
+ if (mType == FormControlType::InputPassword) {
+ // Don't fire another event if we have a pending event.
+ if (aForm && aForm->mHasPendingPasswordEvent) {
+ return;
+ }
+
+ // TODO(Bug 1864404): Use one event for formless and form inputs.
+ eventType = aForm ? u"DOMFormHasPassword"_ns : u"DOMInputPasswordAdded"_ns;
+
+ target = aForm ? static_cast<Element*>(aForm) : this;
+
+ if (aForm) {
+ aForm->mHasPendingPasswordEvent = true;
+ }
+
+ } else if (mType == FormControlType::InputEmail ||
+ mType == FormControlType::InputText) {
+ // Don't fire a username event if:
+ // - <input> is not part of a form
+ // - we have a pending event
+ // - username only forms are not supported
+ if (!aForm || aForm->mHasPendingPossibleUsernameEvent ||
+ !StaticPrefs::signon_usernameOnlyForm_enabled()) {
+ return;
+ }
+
+ eventType = u"DOMFormHasPossibleUsername"_ns;
+ target = aForm;
+
+ aForm->mHasPendingPossibleUsernameEvent = true;
+
+ } else {
+ return;
+ }
+
+ RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher(
+ target, eventType, CanBubble::eYes, ChromeOnlyDispatch::eYes);
+ dispatcher->PostDOMEvent();
+}
+
+void HTMLInputElement::UnbindFromTree(bool aNullParent) {
+ if (mType == FormControlType::InputPassword) {
+ MaybeFireInputPasswordRemoved();
+ }
+
+ // If we have a form and are unbound from it,
+ // nsGenericHTMLFormControlElementWithState::UnbindFromTree() will unset the
+ // form and that takes care of form's WillRemove so we just have to take care
+ // of the case where we're removing from the document and we don't
+ // have a form
+ if (!mForm && mType == FormControlType::InputRadio) {
+ RemoveFromRadioGroup();
+ }
+
+ if (CreatesDateTimeWidget() && IsInComposedDoc()) {
+ NotifyUAWidgetTeardown();
+ }
+
+ nsImageLoadingContent::UnbindFromTree(aNullParent);
+ nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent);
+
+ // If we are contained within a disconnected subtree, attempt to add
+ // ourselves to the subtree root's radio group.
+ if (!mForm && mType == FormControlType::InputRadio) {
+ AddToRadioGroup();
+ }
+
+ // GetCurrentDoc is returning nullptr so we can update the value
+ // missing validity state to reflect we are no longer into a doc.
+ UpdateValueMissingValidityState();
+ // We might be no longer disabled because of parent chain changed.
+ UpdateBarredFromConstraintValidation();
+ // And now make sure our state is up to date
+ UpdateValidityElementStates(false);
+}
+
+/**
+ * @param aType InputElementTypes
+ * @return true, iff SetRangeText applies to aType as specified at
+ * https://html.spec.whatwg.org/#concept-input-apply.
+ */
+static bool SetRangeTextApplies(FormControlType aType) {
+ return aType == FormControlType::InputText ||
+ aType == FormControlType::InputSearch ||
+ aType == FormControlType::InputUrl ||
+ aType == FormControlType::InputTel ||
+ aType == FormControlType::InputPassword;
+}
+
+void HTMLInputElement::HandleTypeChange(FormControlType aNewType,
+ bool aNotify) {
+ FormControlType oldType = mType;
+ MOZ_ASSERT(oldType != aNewType);
+
+ mHasBeenTypePassword =
+ mHasBeenTypePassword || aNewType == FormControlType::InputPassword;
+
+ if (nsFocusManager* fm = nsFocusManager::GetFocusManager()) {
+ // Input element can represent very different kinds of UIs, and we may
+ // need to flush styling even when focusing the already focused input
+ // element.
+ fm->NeedsFlushBeforeEventHandling(this);
+ }
+
+ if (oldType == FormControlType::InputPassword &&
+ State().HasState(ElementState::REVEALED)) {
+ // Modify the state directly to avoid dispatching events.
+ RemoveStates(ElementState::REVEALED, aNotify);
+ }
+
+ if (aNewType == FormControlType::InputFile ||
+ oldType == FormControlType::InputFile) {
+ if (aNewType == FormControlType::InputFile) {
+ mFileData.reset(new FileData());
+ } else {
+ mFileData->Unlink();
+ mFileData = nullptr;
+ }
+ }
+
+ if (oldType == FormControlType::InputRange && mIsDraggingRange) {
+ CancelRangeThumbDrag(false);
+ }
+
+ const ValueModeType oldValueMode = GetValueMode();
+ nsAutoString oldValue;
+ if (oldValueMode == VALUE_MODE_VALUE) {
+ // Doesn't matter what caller type we pass here, since we know we're not a
+ // file input anyway.
+ GetValue(oldValue, CallerType::NonSystem);
+ }
+
+ TextControlState::SelectionProperties sp;
+
+ if (IsSingleLineTextControl(false) && mInputData.mState) {
+ mInputData.mState->SyncUpSelectionPropertiesBeforeDestruction();
+ sp = mInputData.mState->GetSelectionProperties();
+ }
+
+ // We already have a copy of the value, lets free it and changes the type.
+ FreeData();
+ mType = aNewType;
+ void* memory = mInputTypeMem;
+ mInputType = InputType::Create(this, mType, memory);
+
+ if (IsSingleLineTextControl()) {
+ mInputData.mState = TextControlState::Construct(this);
+ if (!sp.IsDefault()) {
+ mInputData.mState->SetSelectionProperties(sp);
+ }
+ }
+
+ // Whether placeholder applies might have changed.
+ UpdatePlaceholderShownState();
+ // Whether readonly applies might have changed.
+ UpdateReadOnlyState(aNotify);
+ UpdateCheckedState(aNotify);
+ UpdateIndeterminateState(aNotify);
+ const bool isDefault = IsRadioOrCheckbox()
+ ? DefaultChecked()
+ : (mForm && mForm->IsDefaultSubmitElement(this));
+ SetStates(ElementState::DEFAULT, isDefault, aNotify);
+
+ // https://html.spec.whatwg.org/#input-type-change
+ switch (GetValueMode()) {
+ case VALUE_MODE_DEFAULT:
+ case VALUE_MODE_DEFAULT_ON:
+ // 1. If the previous state of the element's type attribute put the value
+ // IDL attribute in the value mode, and the element's value is not the
+ // empty string, and the new state of the element's type attribute puts
+ // the value IDL attribute in either the default mode or the default/on
+ // mode, then set the element's value content attribute to the
+ // element's value.
+ if (oldValueMode == VALUE_MODE_VALUE && !oldValue.IsEmpty()) {
+ SetAttr(kNameSpaceID_None, nsGkAtoms::value, oldValue, true);
+ }
+ break;
+ case VALUE_MODE_VALUE: {
+ ValueSetterOptions options{ValueSetterOption::ByInternalAPI};
+ if (!SetRangeTextApplies(oldType) && SetRangeTextApplies(mType)) {
+ options +=
+ ValueSetterOption::MoveCursorToBeginSetSelectionDirectionForward;
+ }
+ if (oldValueMode != VALUE_MODE_VALUE) {
+ // 2. Otherwise, if the previous state of the element's type attribute
+ // put the value IDL attribute in any mode other than the value
+ // mode, and the new state of the element's type attribute puts the
+ // value IDL attribute in the value mode, then set the value of the
+ // element to the value of the value content attribute, if there is
+ // one, or the empty string otherwise, and then set the control's
+ // dirty value flag to false.
+ nsAutoString value;
+ GetAttr(nsGkAtoms::value, value);
+ SetValueInternal(value, options);
+ SetValueChanged(false);
+ } else if (mValueChanged) {
+ // We're both in the "value" mode state, we need to make no change per
+ // spec, but due to how we store the value internally we need to call
+ // SetValueInternal, if our value had changed at all.
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // may potentially be big, but most likely we've failed to allocate
+ // before the type change.)
+ SetValueInternal(oldValue, options);
+ } else {
+ // The value dirty flag is not set, so our value is based on our default
+ // value. But our default value might be dependent on the type. Make
+ // sure to set it so that state is consistent.
+ SetDefaultValueAsValue();
+ }
+ break;
+ }
+ case VALUE_MODE_FILENAME:
+ default:
+ // 3. Otherwise, if the previous state of the element's type attribute
+ // put the value IDL attribute in any mode other than the filename
+ // mode, and the new state of the element's type attribute puts the
+ // value IDL attribute in the filename mode, then set the value of the
+ // element to the empty string.
+ //
+ // Setting the attribute to the empty string is basically calling
+ // ClearFiles, but there can't be any files.
+ break;
+ }
+
+ // Updating mFocusedValue in consequence:
+ // If the new type fires a change event on blur, but the previous type
+ // doesn't, we should set mFocusedValue to the current value.
+ // Otherwise, if the new type doesn't fire a change event on blur, but the
+ // previous type does, we should clear out mFocusedValue.
+ if (MayFireChangeOnBlur(mType) && !MayFireChangeOnBlur(oldType)) {
+ GetValue(mFocusedValue, CallerType::System);
+ } else if (!IsSingleLineTextControl(false, mType) &&
+ IsSingleLineTextControl(false, oldType)) {
+ mFocusedValue.Truncate();
+ }
+
+ // Update or clear our required states since we may have changed from a
+ // required input type to a non-required input type or viceversa.
+ if (DoesRequiredApply()) {
+ const bool isRequired = HasAttr(nsGkAtoms::required);
+ UpdateRequiredState(isRequired, aNotify);
+ } else {
+ RemoveStates(ElementState::REQUIRED_STATES, aNotify);
+ }
+
+ UpdateHasRange(aNotify);
+
+ // Update validity states, but not element state. We'll update
+ // element state later, as part of this attribute change.
+ UpdateAllValidityStatesButNotElementState();
+
+ UpdateApzAwareFlag();
+
+ UpdateBarredFromConstraintValidation();
+
+ // Changing type may affect auto directionality, or non-auto directionality
+ // because of the special-case for <input type=tel>, as specified in
+ // https://html.spec.whatwg.org/multipage/dom.html#the-directionality
+ if (HasDirAuto()) {
+ const bool autoDirAssociated = IsAutoDirectionalityAssociated(mType);
+ if (IsAutoDirectionalityAssociated(oldType) != autoDirAssociated) {
+ SetAutoDirectionality(aNotify);
+ }
+ } else if (oldType == FormControlType::InputTel ||
+ mType == FormControlType::InputTel) {
+ RecomputeDirectionality(this, aNotify);
+ }
+
+ if (oldType == FormControlType::InputImage ||
+ mType == FormControlType::InputImage) {
+ if (oldType == FormControlType::InputImage) {
+ // We're no longer an image input. Cancel our image requests, if we have
+ // any.
+ CancelImageRequests(aNotify);
+ RemoveStates(ElementState::BROKEN, aNotify);
+ } else {
+ // We just got switched to be an image input; we should see whether we
+ // have an image to load;
+ bool hasSrc = false;
+ if (aNotify) {
+ nsAutoString src;
+ if ((hasSrc = GetAttr(nsGkAtoms::src, src))) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ LoadImage(src, false, aNotify, eImageLoadType_Normal,
+ mSrcTriggeringPrincipal);
+ }
+ } else {
+ hasSrc = HasAttr(nsGkAtoms::src);
+ }
+ if (!hasSrc) {
+ AddStates(ElementState::BROKEN, aNotify);
+ }
+ }
+ // We should update our mapped attribute mapping function.
+ if (mAttrs.HasAttrs() && !mAttrs.IsPendingMappedAttributeEvaluation()) {
+ mAttrs.InfallibleMarkAsPendingPresAttributeEvaluation();
+ if (auto* doc = GetComposedDoc()) {
+ doc->ScheduleForPresAttrEvaluation(this);
+ }
+ }
+ }
+
+ MaybeDispatchLoginManagerEvents(mForm);
+
+ if (IsInComposedDoc()) {
+ if (CreatesDateTimeWidget(oldType)) {
+ if (!CreatesDateTimeWidget()) {
+ // Switch away from date/time type.
+ NotifyUAWidgetTeardown();
+ } else {
+ // Switch between date and time.
+ NotifyUAWidgetSetupOrChange();
+ }
+ } else if (CreatesDateTimeWidget()) {
+ // Switch to date/time type.
+ AttachAndSetUAShadowRoot(NotifyUAWidgetSetup::Yes, DelegatesFocus::Yes);
+ }
+ // If we're becoming a text control and have focus, make sure to show focus
+ // rings.
+ if (State().HasState(ElementState::FOCUS) && IsSingleLineTextControl() &&
+ !IsSingleLineTextControl(/* aExcludePassword = */ false, oldType)) {
+ AddStates(ElementState::FOCUSRING);
+ }
+ }
+}
+
+void HTMLInputElement::MaybeSnapToTickMark(Decimal& aValue) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame());
+ if (!rangeFrame) {
+ return;
+ }
+ auto tickMark = rangeFrame->NearestTickMark(aValue);
+ if (tickMark.isNaN()) {
+ return;
+ }
+ auto rangeFrameSize = CSSPixel::FromAppUnits(rangeFrame->GetSize());
+ CSSCoord rangeTrackLength;
+ if (rangeFrame->IsHorizontal()) {
+ rangeTrackLength = rangeFrameSize.width;
+ } else {
+ rangeTrackLength = rangeFrameSize.height;
+ }
+ auto stepBase = GetStepBase();
+ auto distanceToTickMark =
+ rangeTrackLength * float(rangeFrame->GetDoubleAsFractionOfRange(
+ stepBase + (tickMark - aValue).abs()));
+ const CSSCoord magnetEffectRange(
+ StaticPrefs::dom_range_element_magnet_effect_threshold());
+ if (distanceToTickMark <= magnetEffectRange) {
+ aValue = tickMark;
+ }
+}
+
+void HTMLInputElement::SanitizeValue(nsAString& aValue,
+ SanitizationKind aKind) const {
+ NS_ASSERTION(mDoneCreating, "The element creation should be finished!");
+
+ switch (mType) {
+ case FormControlType::InputText:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputPassword: {
+ aValue.StripCRLF();
+ } break;
+ case FormControlType::InputEmail: {
+ aValue.StripCRLF();
+ aValue = nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
+ aValue);
+
+ if (Multiple() && !aValue.IsEmpty()) {
+ nsAutoString oldValue(aValue);
+ HTMLSplitOnSpacesTokenizer tokenizer(oldValue, ',');
+ aValue.Truncate(0);
+ aValue.Append(tokenizer.nextToken());
+ while (tokenizer.hasMoreTokens() ||
+ tokenizer.separatorAfterCurrentToken()) {
+ aValue.Append(',');
+ aValue.Append(tokenizer.nextToken());
+ }
+ }
+ } break;
+ case FormControlType::InputUrl: {
+ aValue.StripCRLF();
+
+ aValue = nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
+ aValue);
+ } break;
+ case FormControlType::InputNumber: {
+ if (aKind == SanitizationKind::ForValueSetter && !aValue.IsEmpty() &&
+ (aValue.First() == '+' || aValue.Last() == '.')) {
+ // A value with a leading plus or trailing dot should fail to parse.
+ // However, the localized parser accepts this, and when we convert it
+ // back to a Decimal, it disappears. So, we need to check first.
+ //
+ // FIXME(emilio): Should we just use the unlocalized parser
+ // (StringToDecimal) for the value setter? Other browsers don't seem to
+ // allow setting localized strings there, and that way we don't need
+ // this special-case.
+ aValue.Truncate();
+ return;
+ }
+
+ InputType::StringToNumberResult result =
+ mInputType->ConvertStringToNumber(aValue);
+ if (!result.mResult.isFinite()) {
+ aValue.Truncate();
+ return;
+ }
+ switch (aKind) {
+ case SanitizationKind::ForValueGetter: {
+ // If the default non-localized algorithm parses the value, then we're
+ // done, don't un-localize it, to avoid precision loss, and to
+ // preserve scientific notation as well for example.
+ if (!result.mLocalized) {
+ return;
+ }
+ // For the <input type=number> value getter, we return the unlocalized
+ // value if it doesn't parse as StringToDecimal, for compat with other
+ // browsers.
+ char buf[32];
+ DebugOnly<bool> ok = result.mResult.toString(buf, ArrayLength(buf));
+ aValue.AssignASCII(buf);
+ MOZ_ASSERT(ok, "buf not big enough");
+ break;
+ }
+ case SanitizationKind::ForDisplay:
+ case SanitizationKind::ForValueSetter: {
+ // We localize as needed, but if both the localized and unlocalized
+ // version parse with the generic parser, we just use the unlocalized
+ // one, to preserve the input as much as possible.
+ //
+ // FIXME(emilio, bug 1622808): Localization should ideally be more
+ // input-preserving.
+ nsString localizedValue;
+ mInputType->ConvertNumberToString(result.mResult, localizedValue);
+ if (!StringToDecimal(localizedValue).isFinite()) {
+ aValue = std::move(localizedValue);
+ }
+ break;
+ }
+ }
+ break;
+ }
+ case FormControlType::InputRange: {
+ Decimal minimum = GetMinimum();
+ Decimal maximum = GetMaximum();
+ MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(),
+ "type=range should have a default maximum/minimum");
+
+ // We use this to avoid modifying the string unnecessarily, since that
+ // may introduce rounding. This is set to true only if the value we
+ // parse out from aValue needs to be sanitized.
+ bool needSanitization = false;
+
+ Decimal value = mInputType->ConvertStringToNumber(aValue).mResult;
+ if (!value.isFinite()) {
+ needSanitization = true;
+ // Set value to midway between minimum and maximum.
+ value = maximum <= minimum ? minimum
+ : minimum + (maximum - minimum) / Decimal(2);
+ } else if (value < minimum || maximum < minimum) {
+ needSanitization = true;
+ value = minimum;
+ } else if (value > maximum) {
+ needSanitization = true;
+ value = maximum;
+ }
+
+ Decimal step = GetStep();
+ if (step != kStepAny) {
+ Decimal stepBase = GetStepBase();
+ // There could be rounding issues below when dealing with fractional
+ // numbers, but let's ignore that until ECMAScript supplies us with a
+ // decimal number type.
+ Decimal deltaToStep = NS_floorModulo(value - stepBase, step);
+ if (deltaToStep != Decimal(0)) {
+ // "suffering from a step mismatch"
+ // Round the element's value to the nearest number for which the
+ // element would not suffer from a step mismatch, and which is
+ // greater than or equal to the minimum, and, if the maximum is not
+ // less than the minimum, which is less than or equal to the
+ // maximum, if there is a number that matches these constraints:
+ MOZ_ASSERT(deltaToStep > Decimal(0),
+ "stepBelow/stepAbove will be wrong");
+ Decimal stepBelow = value - deltaToStep;
+ Decimal stepAbove = value - deltaToStep + step;
+ Decimal halfStep = step / Decimal(2);
+ bool stepAboveIsClosest = (stepAbove - value) <= halfStep;
+ bool stepAboveInRange = stepAbove >= minimum && stepAbove <= maximum;
+ bool stepBelowInRange = stepBelow >= minimum && stepBelow <= maximum;
+
+ if ((stepAboveIsClosest || !stepBelowInRange) && stepAboveInRange) {
+ needSanitization = true;
+ value = stepAbove;
+ } else if ((!stepAboveIsClosest || !stepAboveInRange) &&
+ stepBelowInRange) {
+ needSanitization = true;
+ value = stepBelow;
+ }
+ }
+ }
+
+ if (needSanitization) {
+ char buf[32];
+ DebugOnly<bool> ok = value.toString(buf, ArrayLength(buf));
+ aValue.AssignASCII(buf);
+ MOZ_ASSERT(ok, "buf not big enough");
+ }
+ } break;
+ case FormControlType::InputDate: {
+ if (!aValue.IsEmpty() && !IsValidDate(aValue)) {
+ aValue.Truncate();
+ }
+ } break;
+ case FormControlType::InputTime: {
+ if (!aValue.IsEmpty() && !IsValidTime(aValue)) {
+ aValue.Truncate();
+ }
+ } break;
+ case FormControlType::InputMonth: {
+ if (!aValue.IsEmpty() && !IsValidMonth(aValue)) {
+ aValue.Truncate();
+ }
+ } break;
+ case FormControlType::InputWeek: {
+ if (!aValue.IsEmpty() && !IsValidWeek(aValue)) {
+ aValue.Truncate();
+ }
+ } break;
+ case FormControlType::InputDatetimeLocal: {
+ if (!aValue.IsEmpty() && !IsValidDateTimeLocal(aValue)) {
+ aValue.Truncate();
+ } else {
+ NormalizeDateTimeLocal(aValue);
+ }
+ } break;
+ case FormControlType::InputColor: {
+ if (IsValidSimpleColor(aValue)) {
+ ToLowerCase(aValue);
+ } else {
+ // Set default (black) color, if aValue wasn't parsed correctly.
+ aValue.AssignLiteral("#000000");
+ }
+ } break;
+ default:
+ break;
+ }
+}
+
+Maybe<nscolor> HTMLInputElement::ParseSimpleColor(const nsAString& aColor) {
+ // Input color string should be 7 length (i.e. a string representing a valid
+ // simple color)
+ if (aColor.Length() != 7 || aColor.First() != '#') {
+ return {};
+ }
+
+ const nsAString& withoutHash = StringTail(aColor, 6);
+ nscolor color;
+ if (!NS_HexToRGBA(withoutHash, nsHexColorType::NoAlpha, &color)) {
+ return {};
+ }
+
+ return Some(color);
+}
+
+bool HTMLInputElement::IsValidSimpleColor(const nsAString& aValue) const {
+ if (aValue.Length() != 7 || aValue.First() != '#') {
+ return false;
+ }
+
+ for (int i = 1; i < 7; ++i) {
+ if (!IsAsciiDigit(aValue[i]) && !(aValue[i] >= 'a' && aValue[i] <= 'f') &&
+ !(aValue[i] >= 'A' && aValue[i] <= 'F')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool HTMLInputElement::IsLeapYear(uint32_t aYear) const {
+ if ((aYear % 4 == 0 && aYear % 100 != 0) || (aYear % 400 == 0)) {
+ return true;
+ }
+ return false;
+}
+
+uint32_t HTMLInputElement::DayOfWeek(uint32_t aYear, uint32_t aMonth,
+ uint32_t aDay, bool isoWeek) const {
+ MOZ_ASSERT(1 <= aMonth && aMonth <= 12, "month is in 1..12");
+ MOZ_ASSERT(1 <= aDay && aDay <= 31, "day is in 1..31");
+
+ // Tomohiko Sakamoto algorithm.
+ int monthTable[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
+ aYear -= aMonth < 3;
+
+ uint32_t day = (aYear + aYear / 4 - aYear / 100 + aYear / 400 +
+ monthTable[aMonth - 1] + aDay) %
+ 7;
+
+ if (isoWeek) {
+ return ((day + 6) % 7) + 1;
+ }
+
+ return day;
+}
+
+uint32_t HTMLInputElement::MaximumWeekInYear(uint32_t aYear) const {
+ int day = DayOfWeek(aYear, 1, 1, true); // January 1.
+ // A year starting on Thursday or a leap year starting on Wednesday has 53
+ // weeks. All other years have 52 weeks.
+ return day == 4 || (day == 3 && IsLeapYear(aYear)) ? kMaximumWeekInYear
+ : kMaximumWeekInYear - 1;
+}
+
+bool HTMLInputElement::IsValidWeek(const nsAString& aValue) const {
+ uint32_t year, week;
+ return ParseWeek(aValue, &year, &week);
+}
+
+bool HTMLInputElement::IsValidMonth(const nsAString& aValue) const {
+ uint32_t year, month;
+ return ParseMonth(aValue, &year, &month);
+}
+
+bool HTMLInputElement::IsValidDate(const nsAString& aValue) const {
+ uint32_t year, month, day;
+ return ParseDate(aValue, &year, &month, &day);
+}
+
+bool HTMLInputElement::IsValidDateTimeLocal(const nsAString& aValue) const {
+ uint32_t year, month, day, time;
+ return ParseDateTimeLocal(aValue, &year, &month, &day, &time);
+}
+
+bool HTMLInputElement::ParseYear(const nsAString& aValue,
+ uint32_t* aYear) const {
+ if (aValue.Length() < 4) {
+ return false;
+ }
+
+ return DigitSubStringToNumber(aValue, 0, aValue.Length(), aYear) &&
+ *aYear > 0;
+}
+
+bool HTMLInputElement::ParseMonth(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth) const {
+ // Parse the year, month values out a string formatted as 'yyyy-mm'.
+ if (aValue.Length() < 7) {
+ return false;
+ }
+
+ uint32_t endOfYearOffset = aValue.Length() - 3;
+ if (aValue[endOfYearOffset] != '-') {
+ return false;
+ }
+
+ const nsAString& yearStr = Substring(aValue, 0, endOfYearOffset);
+ if (!ParseYear(yearStr, aYear)) {
+ return false;
+ }
+
+ return DigitSubStringToNumber(aValue, endOfYearOffset + 1, 2, aMonth) &&
+ *aMonth > 0 && *aMonth <= 12;
+}
+
+bool HTMLInputElement::ParseWeek(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aWeek) const {
+ // Parse the year, month values out a string formatted as 'yyyy-Www'.
+ if (aValue.Length() < 8) {
+ return false;
+ }
+
+ uint32_t endOfYearOffset = aValue.Length() - 4;
+ if (aValue[endOfYearOffset] != '-') {
+ return false;
+ }
+
+ if (aValue[endOfYearOffset + 1] != 'W') {
+ return false;
+ }
+
+ const nsAString& yearStr = Substring(aValue, 0, endOfYearOffset);
+ if (!ParseYear(yearStr, aYear)) {
+ return false;
+ }
+
+ return DigitSubStringToNumber(aValue, endOfYearOffset + 2, 2, aWeek) &&
+ *aWeek > 0 && *aWeek <= MaximumWeekInYear(*aYear);
+}
+
+bool HTMLInputElement::ParseDate(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth, uint32_t* aDay) const {
+ /*
+ * Parse the year, month, day values out a date string formatted as
+ * yyyy-mm-dd. -The year must be 4 or more digits long, and year > 0 -The
+ * month must be exactly 2 digits long, and 01 <= month <= 12 -The day must be
+ * exactly 2 digit long, and 01 <= day <= maxday Where maxday is the number of
+ * days in the month 'month' and year 'year'
+ */
+ if (aValue.Length() < 10) {
+ return false;
+ }
+
+ uint32_t endOfMonthOffset = aValue.Length() - 3;
+ if (aValue[endOfMonthOffset] != '-') {
+ return false;
+ }
+
+ const nsAString& yearMonthStr = Substring(aValue, 0, endOfMonthOffset);
+ if (!ParseMonth(yearMonthStr, aYear, aMonth)) {
+ return false;
+ }
+
+ return DigitSubStringToNumber(aValue, endOfMonthOffset + 1, 2, aDay) &&
+ *aDay > 0 && *aDay <= NumberOfDaysInMonth(*aMonth, *aYear);
+}
+
+bool HTMLInputElement::ParseDateTimeLocal(const nsAString& aValue,
+ uint32_t* aYear, uint32_t* aMonth,
+ uint32_t* aDay,
+ uint32_t* aTime) const {
+ // Parse the year, month, day and time values out a string formatted as
+ // 'yyyy-mm-ddThh:mm[:ss.s] or 'yyyy-mm-dd hh:mm[:ss.s]', where fractions of
+ // seconds can be 1 to 3 digits.
+ // The minimum length allowed is 16, which is of the form 'yyyy-mm-ddThh:mm'
+ // or 'yyyy-mm-dd hh:mm'.
+ if (aValue.Length() < 16) {
+ return false;
+ }
+
+ int32_t sepIndex = aValue.FindChar('T');
+ if (sepIndex == -1) {
+ sepIndex = aValue.FindChar(' ');
+
+ if (sepIndex == -1) {
+ return false;
+ }
+ }
+
+ const nsAString& dateStr = Substring(aValue, 0, sepIndex);
+ if (!ParseDate(dateStr, aYear, aMonth, aDay)) {
+ return false;
+ }
+
+ const nsAString& timeStr =
+ Substring(aValue, sepIndex + 1, aValue.Length() - sepIndex + 1);
+ if (!ParseTime(timeStr, aTime)) {
+ return false;
+ }
+
+ return true;
+}
+
+void HTMLInputElement::NormalizeDateTimeLocal(nsAString& aValue) const {
+ if (aValue.IsEmpty()) {
+ return;
+ }
+
+ // Use 'T' as the separator between date string and time string.
+ int32_t sepIndex = aValue.FindChar(' ');
+ if (sepIndex != -1) {
+ aValue.ReplaceLiteral(sepIndex, 1, u"T");
+ } else {
+ sepIndex = aValue.FindChar('T');
+ }
+
+ // Time expressed as the shortest possible string, which is hh:mm.
+ if ((aValue.Length() - sepIndex) == 6) {
+ return;
+ }
+
+ // Fractions of seconds part is optional, ommit it if it's 0.
+ if ((aValue.Length() - sepIndex) > 9) {
+ const uint32_t millisecSepIndex = sepIndex + 9;
+ uint32_t milliseconds;
+ if (!DigitSubStringToNumber(aValue, millisecSepIndex + 1,
+ aValue.Length() - (millisecSepIndex + 1),
+ &milliseconds)) {
+ return;
+ }
+
+ if (milliseconds != 0) {
+ return;
+ }
+
+ aValue.Cut(millisecSepIndex, aValue.Length() - millisecSepIndex);
+ }
+
+ // Seconds part is optional, ommit it if it's 0.
+ const uint32_t secondSepIndex = sepIndex + 6;
+ uint32_t seconds;
+ if (!DigitSubStringToNumber(aValue, secondSepIndex + 1,
+ aValue.Length() - (secondSepIndex + 1),
+ &seconds)) {
+ return;
+ }
+
+ if (seconds != 0) {
+ return;
+ }
+
+ aValue.Cut(secondSepIndex, aValue.Length() - secondSepIndex);
+}
+
+double HTMLInputElement::DaysSinceEpochFromWeek(uint32_t aYear,
+ uint32_t aWeek) const {
+ double days = JS::DayFromYear(aYear) + (aWeek - 1) * 7;
+ uint32_t dayOneIsoWeekday = DayOfWeek(aYear, 1, 1, true);
+
+ // If day one of that year is on/before Thursday, we should subtract the
+ // days that belong to last year in our first week, otherwise, our first
+ // days belong to last year's last week, and we should add those days
+ // back.
+ if (dayOneIsoWeekday <= 4) {
+ days -= (dayOneIsoWeekday - 1);
+ } else {
+ days += (7 - dayOneIsoWeekday + 1);
+ }
+
+ return days;
+}
+
+uint32_t HTMLInputElement::NumberOfDaysInMonth(uint32_t aMonth,
+ uint32_t aYear) const {
+ /*
+ * Returns the number of days in a month.
+ * Months that are |longMonths| always have 31 days.
+ * Months that are not |longMonths| have 30 days except February (month 2).
+ * February has 29 days during leap years which are years that are divisible
+ * by 400. or divisible by 100 and 4. February has 28 days otherwise.
+ */
+
+ static const bool longMonths[] = {true, false, true, false, true, false,
+ true, true, false, true, false, true};
+ MOZ_ASSERT(aMonth <= 12 && aMonth > 0);
+
+ if (longMonths[aMonth - 1]) {
+ return 31;
+ }
+
+ if (aMonth != 2) {
+ return 30;
+ }
+
+ return IsLeapYear(aYear) ? 29 : 28;
+}
+
+/* static */
+bool HTMLInputElement::DigitSubStringToNumber(const nsAString& aStr,
+ uint32_t aStart, uint32_t aLen,
+ uint32_t* aRetVal) {
+ MOZ_ASSERT(aStr.Length() > (aStart + aLen - 1));
+
+ for (uint32_t offset = 0; offset < aLen; ++offset) {
+ if (!IsAsciiDigit(aStr[aStart + offset])) {
+ return false;
+ }
+ }
+
+ nsresult ec;
+ *aRetVal = static_cast<uint32_t>(
+ PromiseFlatString(Substring(aStr, aStart, aLen)).ToInteger(&ec));
+
+ return NS_SUCCEEDED(ec);
+}
+
+bool HTMLInputElement::IsValidTime(const nsAString& aValue) const {
+ return ParseTime(aValue, nullptr);
+}
+
+/* static */
+bool HTMLInputElement::ParseTime(const nsAString& aValue, uint32_t* aResult) {
+ /* The string must have the following parts:
+ * - HOURS: two digits, value being in [0, 23];
+ * - Colon (:);
+ * - MINUTES: two digits, value being in [0, 59];
+ * - Optional:
+ * - Colon (:);
+ * - SECONDS: two digits, value being in [0, 59];
+ * - Optional:
+ * - DOT (.);
+ * - FRACTIONAL SECONDS: one to three digits, no value range.
+ */
+
+ // The following format is the shorter one allowed: "HH:MM".
+ if (aValue.Length() < 5) {
+ return false;
+ }
+
+ uint32_t hours;
+ if (!DigitSubStringToNumber(aValue, 0, 2, &hours) || hours > 23) {
+ return false;
+ }
+
+ // Hours/minutes separator.
+ if (aValue[2] != ':') {
+ return false;
+ }
+
+ uint32_t minutes;
+ if (!DigitSubStringToNumber(aValue, 3, 2, &minutes) || minutes > 59) {
+ return false;
+ }
+
+ if (aValue.Length() == 5) {
+ if (aResult) {
+ *aResult = ((hours * 60) + minutes) * 60000;
+ }
+ return true;
+ }
+
+ // The following format is the next shorter one: "HH:MM:SS".
+ if (aValue.Length() < 8 || aValue[5] != ':') {
+ return false;
+ }
+
+ uint32_t seconds;
+ if (!DigitSubStringToNumber(aValue, 6, 2, &seconds) || seconds > 59) {
+ return false;
+ }
+
+ if (aValue.Length() == 8) {
+ if (aResult) {
+ *aResult = (((hours * 60) + minutes) * 60 + seconds) * 1000;
+ }
+ return true;
+ }
+
+ // The string must follow this format now: "HH:MM:SS.{s,ss,sss}".
+ // There can be 1 to 3 digits for the fractions of seconds.
+ if (aValue.Length() == 9 || aValue.Length() > 12 || aValue[8] != '.') {
+ return false;
+ }
+
+ uint32_t fractionsSeconds;
+ if (!DigitSubStringToNumber(aValue, 9, aValue.Length() - 9,
+ &fractionsSeconds)) {
+ return false;
+ }
+
+ if (aResult) {
+ *aResult = (((hours * 60) + minutes) * 60 + seconds) * 1000 +
+ // NOTE: there is 10.0 instead of 10 and static_cast<int> because
+ // some old [and stupid] compilers can't just do the right thing.
+ fractionsSeconds *
+ pow(10.0, static_cast<int>(3 - (aValue.Length() - 9)));
+ }
+
+ return true;
+}
+
+/* static */
+bool HTMLInputElement::IsDateTimeTypeSupported(
+ FormControlType aDateTimeInputType) {
+ switch (aDateTimeInputType) {
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ return StaticPrefs::dom_forms_datetime_others();
+ default:
+ return false;
+ }
+}
+
+void HTMLInputElement::GetLastInteractiveValue(nsAString& aValue) {
+ if (mLastValueChangeWasInteractive) {
+ return GetValue(aValue, CallerType::System);
+ }
+ if (TextControlState* state = GetEditorState()) {
+ return aValue.Assign(
+ state->LastInteractiveValueIfLastChangeWasNonInteractive());
+ }
+ aValue.Truncate();
+}
+
+bool HTMLInputElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ // We can't make these static_asserts because kInputDefaultType and
+ // kInputTypeTable aren't constexpr.
+ MOZ_ASSERT(
+ FormControlType(kInputDefaultType->value) == FormControlType::InputText,
+ "Someone forgot to update kInputDefaultType when adding a new "
+ "input type.");
+ MOZ_ASSERT(kInputTypeTable[ArrayLength(kInputTypeTable) - 1].tag == nullptr,
+ "Last entry in the table must be the nullptr guard");
+ MOZ_ASSERT(FormControlType(
+ kInputTypeTable[ArrayLength(kInputTypeTable) - 2].value) ==
+ FormControlType::InputText,
+ "Next to last entry in the table must be the \"text\" entry");
+
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::type) {
+ aResult.ParseEnumValue(aValue, kInputTypeTable, false, kInputDefaultType);
+ auto newType = FormControlType(aResult.GetEnumValue());
+ if (IsDateTimeInputType(newType) && !IsDateTimeTypeSupported(newType)) {
+ // There's no public way to set an nsAttrValue to an enum value, but we
+ // can just re-parse with a table that doesn't have any types other than
+ // "text" in it.
+ aResult.ParseEnumValue(aValue, kInputDefaultType, false,
+ kInputDefaultType);
+ }
+
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::width) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::height) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::maxlength) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::minlength) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::size) {
+ return aResult.ParsePositiveIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::formmethod) {
+ return aResult.ParseEnumValue(aValue, kFormMethodTable, false);
+ }
+ if (aAttribute == nsGkAtoms::formenctype) {
+ return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false);
+ }
+ if (aAttribute == nsGkAtoms::autocomplete) {
+ aResult.ParseAtomArray(aValue);
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::capture) {
+ return aResult.ParseEnumValue(aValue, kCaptureTable, false,
+ kCaptureDefault);
+ }
+ if (ParseImageAttribute(aAttribute, aValue, aResult)) {
+ // We have to call |ParseImageAttribute| unconditionally since we
+ // don't know if we're going to have a type="image" attribute yet,
+ // (or could have it set dynamically in the future). See bug
+ // 214077.
+ return true;
+ }
+ }
+
+ return TextControlElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLInputElement::ImageInputMapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ nsGenericHTMLFormControlElementWithState::MapImageBorderAttributeInto(
+ aBuilder);
+ nsGenericHTMLFormControlElementWithState::MapImageMarginAttributeInto(
+ aBuilder);
+ nsGenericHTMLFormControlElementWithState::MapImageSizeAttributesInto(
+ aBuilder, MapAspectRatio::Yes);
+ // Images treat align as "float"
+ nsGenericHTMLFormControlElementWithState::MapImageAlignAttributeInto(
+ aBuilder);
+ nsGenericHTMLFormControlElementWithState::MapCommonAttributesInto(aBuilder);
+}
+
+nsChangeHint HTMLInputElement::GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLFormControlElementWithState::GetAttributeChangeHint(
+ aAttribute, aModType);
+
+ const bool isAdditionOrRemoval =
+ aModType == MutationEvent_Binding::ADDITION ||
+ aModType == MutationEvent_Binding::REMOVAL;
+
+ const bool reconstruct = [&] {
+ if (aAttribute == nsGkAtoms::type) {
+ return true;
+ }
+
+ if (PlaceholderApplies() && aAttribute == nsGkAtoms::placeholder &&
+ isAdditionOrRemoval) {
+ // We need to re-create our placeholder text.
+ return true;
+ }
+
+ if (mType == FormControlType::InputFile &&
+ aAttribute == nsGkAtoms::webkitdirectory) {
+ // The presence or absence of the 'directory' attribute determines what
+ // value we show in the file label when empty, via GetDisplayFileName.
+ return true;
+ }
+
+ if (mType == FormControlType::InputImage && isAdditionOrRemoval &&
+ (aAttribute == nsGkAtoms::alt || aAttribute == nsGkAtoms::value)) {
+ // We might need to rebuild our alt text. Just go ahead and
+ // reconstruct our frame. This should be quite rare..
+ return true;
+ }
+ return false;
+ }();
+
+ if (reconstruct) {
+ retval |= nsChangeHint_ReconstructFrame;
+ } else if (aAttribute == nsGkAtoms::value) {
+ retval |= NS_STYLE_HINT_REFLOW;
+ } else if (aAttribute == nsGkAtoms::size && IsSingleLineTextControl(false)) {
+ retval |= NS_STYLE_HINT_REFLOW;
+ }
+
+ return retval;
+}
+
+NS_IMETHODIMP_(bool)
+HTMLInputElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::align},
+ {nullptr},
+ };
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ sImageMarginSizeAttributeMap,
+ sImageBorderAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLInputElement::GetAttributeMappingFunction()
+ const {
+ // GetAttributeChangeHint guarantees that changes to mType will trigger a
+ // reframe, and we update the mapping function in our mapped attrs when our
+ // type changes, so it's safe to condition our attribute mapping function on
+ // mType.
+ if (mType == FormControlType::InputImage) {
+ return &ImageInputMapAttributesIntoRule;
+ }
+
+ return &MapCommonAttributesInto;
+}
+
+// Directory picking methods:
+
+already_AddRefed<Promise> HTMLInputElement::GetFilesAndDirectories(
+ ErrorResult& aRv) {
+ if (mType != FormControlType::InputFile) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ MOZ_ASSERT(global);
+ if (!global) {
+ return nullptr;
+ }
+
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ const nsTArray<OwningFileOrDirectory>& filesAndDirs =
+ GetFilesOrDirectoriesInternal();
+
+ Sequence<OwningFileOrDirectory> filesAndDirsSeq;
+
+ if (!filesAndDirsSeq.SetLength(filesAndDirs.Length(), fallible)) {
+ p->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+ return p.forget();
+ }
+
+ for (uint32_t i = 0; i < filesAndDirs.Length(); ++i) {
+ if (filesAndDirs[i].IsDirectory()) {
+ RefPtr<Directory> directory = filesAndDirs[i].GetAsDirectory();
+
+ // In future we could refactor SetFilePickerFiltersFromAccept to return a
+ // semicolon separated list of file extensions and include that in the
+ // filter string passed here.
+ directory->SetContentFilters(u"filter-out-sensitive"_ns);
+ filesAndDirsSeq[i].SetAsDirectory() = directory;
+ } else {
+ MOZ_ASSERT(filesAndDirs[i].IsFile());
+
+ // This file was directly selected by the user, so don't filter it.
+ filesAndDirsSeq[i].SetAsFile() = filesAndDirs[i].GetAsFile();
+ }
+ }
+
+ p->MaybeResolve(filesAndDirsSeq);
+ return p.forget();
+}
+
+// Controllers Methods
+
+nsIControllers* HTMLInputElement::GetControllers(ErrorResult& aRv) {
+ // XXX: what about type "file"?
+ if (IsSingleLineTextControl(false)) {
+ if (!mControllers) {
+ mControllers = new nsXULControllers();
+ if (!mControllers) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<nsBaseCommandController> commandController =
+ nsBaseCommandController::CreateEditorController();
+ if (!commandController) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ mControllers->AppendController(commandController);
+
+ commandController = nsBaseCommandController::CreateEditingController();
+ if (!commandController) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ mControllers->AppendController(commandController);
+ }
+ }
+
+ return mControllers;
+}
+
+nsresult HTMLInputElement::GetControllers(nsIControllers** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ ErrorResult rv;
+ RefPtr<nsIControllers> controller = GetControllers(rv);
+ controller.forget(aResult);
+ return rv.StealNSResult();
+}
+
+int32_t HTMLInputElement::InputTextLength(CallerType aCallerType) {
+ nsAutoString val;
+ GetValue(val, aCallerType);
+ return val.Length();
+}
+
+void HTMLInputElement::SetSelectionRange(uint32_t aSelectionStart,
+ uint32_t aSelectionEnd,
+ const Optional<nsAString>& aDirection,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetSelectionRange(aSelectionStart, aSelectionEnd, aDirection, aRv);
+}
+
+void HTMLInputElement::SetRangeText(const nsAString& aReplacement,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetRangeText(aReplacement, aRv);
+}
+
+void HTMLInputElement::SetRangeText(const nsAString& aReplacement,
+ uint32_t aStart, uint32_t aEnd,
+ SelectionMode aSelectMode,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetRangeText(aReplacement, aStart, aEnd, aSelectMode, aRv);
+}
+
+void HTMLInputElement::GetValueFromSetRangeText(nsAString& aValue) {
+ GetNonFileValueInternal(aValue);
+}
+
+nsresult HTMLInputElement::SetValueFromSetRangeText(const nsAString& aValue) {
+ return SetValueInternal(aValue, {ValueSetterOption::ByContentAPI,
+ ValueSetterOption::BySetRangeTextAPI,
+ ValueSetterOption::SetValueChanged});
+}
+
+Nullable<uint32_t> HTMLInputElement::GetSelectionStart(ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ return Nullable<uint32_t>();
+ }
+
+ uint32_t selStart = GetSelectionStartIgnoringType(aRv);
+ if (aRv.Failed()) {
+ return Nullable<uint32_t>();
+ }
+
+ return Nullable<uint32_t>(selStart);
+}
+
+uint32_t HTMLInputElement::GetSelectionStartIgnoringType(ErrorResult& aRv) {
+ uint32_t selEnd, selStart;
+ GetSelectionRange(&selStart, &selEnd, aRv);
+ return selStart;
+}
+
+void HTMLInputElement::SetSelectionStart(
+ const Nullable<uint32_t>& aSelectionStart, ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetSelectionStart(aSelectionStart, aRv);
+}
+
+Nullable<uint32_t> HTMLInputElement::GetSelectionEnd(ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ return Nullable<uint32_t>();
+ }
+
+ uint32_t selEnd = GetSelectionEndIgnoringType(aRv);
+ if (aRv.Failed()) {
+ return Nullable<uint32_t>();
+ }
+
+ return Nullable<uint32_t>(selEnd);
+}
+
+uint32_t HTMLInputElement::GetSelectionEndIgnoringType(ErrorResult& aRv) {
+ uint32_t selEnd, selStart;
+ GetSelectionRange(&selStart, &selEnd, aRv);
+ return selEnd;
+}
+
+void HTMLInputElement::SetSelectionEnd(const Nullable<uint32_t>& aSelectionEnd,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetSelectionEnd(aSelectionEnd, aRv);
+}
+
+void HTMLInputElement::GetSelectionRange(uint32_t* aSelectionStart,
+ uint32_t* aSelectionEnd,
+ ErrorResult& aRv) {
+ TextControlState* state = GetEditorState();
+ if (!state) {
+ // Not a text control.
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ state->GetSelectionRange(aSelectionStart, aSelectionEnd, aRv);
+}
+
+void HTMLInputElement::GetSelectionDirection(nsAString& aDirection,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aDirection.SetIsVoid(true);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection came back true!");
+ state->GetSelectionDirectionString(aDirection, aRv);
+}
+
+void HTMLInputElement::SetSelectionDirection(const nsAString& aDirection,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection came back true!");
+ state->SetSelectionDirection(aDirection, aRv);
+}
+
+// https://html.spec.whatwg.org/multipage/input.html#dom-input-showpicker
+void HTMLInputElement::ShowPicker(ErrorResult& aRv) {
+ // Step 1. If this is not mutable, then throw an "InvalidStateError"
+ // DOMException.
+ if (!IsMutable()) {
+ return aRv.ThrowInvalidStateError(
+ "This input is either disabled or readonly.");
+ }
+
+ // Step 2. If this's relevant settings object's origin is not same origin with
+ // this's relevant settings object's top-level origin, and this's type
+ // attribute is not in the File Upload state or Color state, then throw a
+ // "SecurityError" DOMException.
+ if (mType != FormControlType::InputFile &&
+ mType != FormControlType::InputColor) {
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ WindowGlobalChild* windowGlobalChild =
+ window ? window->GetWindowGlobalChild() : nullptr;
+ if (!windowGlobalChild || !windowGlobalChild->SameOriginWithTop()) {
+ return aRv.ThrowSecurityError(
+ "Call was blocked because the current origin isn't same-origin with "
+ "top.");
+ }
+ }
+
+ // Step 3. If this's relevant global object does not have transient
+ // activation, then throw a "NotAllowedError" DOMException.
+ if (!OwnerDoc()->HasValidTransientUserGestureActivation()) {
+ return aRv.ThrowNotAllowedError(
+ "Call was blocked due to lack of user activation.");
+ }
+
+ // Step 4. Show the picker, if applicable, for this.
+ //
+ // https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable
+ // To show the picker, if applicable for an input element element:
+
+ // Step 1. Assert: element's relevant global object has transient activation.
+ // Step 2. If element is not mutable, then return.
+ // (See above.)
+
+ // Step 3. If element's type attribute is in the File Upload state, then run
+ // these steps in parallel:
+ if (mType == FormControlType::InputFile) {
+ FilePickerType type = FILE_PICKER_FILE;
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ HasAttr(nsGkAtoms::webkitdirectory)) {
+ type = FILE_PICKER_DIRECTORY;
+ }
+ InitFilePicker(type);
+ return;
+ }
+
+ // Step 4. Otherwise, the user agent should show any relevant user interface
+ // for selecting a value for element, in the way it normally would when the
+ // user interacts with the control
+ if (mType == FormControlType::InputColor) {
+ InitColorPicker();
+ return;
+ }
+
+ if (!IsInComposedDoc()) {
+ return;
+ }
+
+ if (IsDateTimeTypeSupported(mType)) {
+ if (CreatesDateTimeWidget()) {
+ if (RefPtr<Element> dateTimeBoxElement = GetDateTimeBoxElement()) {
+ // Event is dispatched to closed-shadow tree and doesn't bubble.
+ RefPtr<Document> doc = dateTimeBoxElement->OwnerDoc();
+ nsContentUtils::DispatchTrustedEvent(doc, dateTimeBoxElement,
+ u"MozDateTimeShowPickerForJS"_ns,
+ CanBubble::eNo, Cancelable::eNo);
+ }
+ } else {
+ DateTimeValue value;
+ GetDateTimeInputBoxValue(value);
+ OpenDateTimePicker(value);
+ }
+ }
+}
+
+#ifdef ACCESSIBILITY
+/*static*/ nsresult FireEventForAccessibility(HTMLInputElement* aTarget,
+ EventMessage aEventMessage) {
+ Element* element = static_cast<Element*>(aTarget);
+ return nsContentUtils::DispatchTrustedEvent<WidgetEvent>(
+ element->OwnerDoc(), element, aEventMessage, CanBubble::eYes,
+ Cancelable::eYes);
+}
+#endif
+
+void HTMLInputElement::UpdateApzAwareFlag() {
+#if !defined(ANDROID) && !defined(XP_MACOSX)
+ if (mType == FormControlType::InputNumber ||
+ mType == FormControlType::InputRange) {
+ SetMayBeApzAware();
+ }
+#endif
+}
+
+nsresult HTMLInputElement::SetDefaultValueAsValue() {
+ NS_ASSERTION(GetValueMode() == VALUE_MODE_VALUE,
+ "GetValueMode() should return VALUE_MODE_VALUE!");
+
+ // The element has a content attribute value different from it's value when
+ // it's in the value mode value.
+ nsAutoString resetVal;
+ GetDefaultValue(resetVal);
+
+ // SetValueInternal is going to sanitize the value.
+ // TODO(mbrodesser): sanitizing will only happen if `mDoneCreating` is true.
+ return SetValueInternal(resetVal, ValueSetterOption::ByInternalAPI);
+}
+
+// https://html.spec.whatwg.org/#auto-directionality
+void HTMLInputElement::SetAutoDirectionality(bool aNotify,
+ const nsAString* aKnownValue) {
+ if (!IsAutoDirectionalityAssociated()) {
+ return SetDirectionality(GetParentDirectionality(this), aNotify);
+ }
+ nsAutoString value;
+ if (!aKnownValue) {
+ // It's unclear if per spec we should use the sanitized or unsanitized
+ // value to set the directionality, but aKnownValue is unsanitized, so be
+ // consistent. Using what the user is seeing to determine directionality
+ // instead of the sanitized (empty if invalid) value probably makes more
+ // sense.
+ GetValueInternal(value, CallerType::System);
+ aKnownValue = &value;
+ }
+ SetDirectionalityFromValue(this, *aKnownValue, aNotify);
+}
+
+NS_IMETHODIMP
+HTMLInputElement::Reset() {
+ // We should be able to reset all dirty flags regardless of the type.
+ SetCheckedChanged(false);
+ SetValueChanged(false);
+ SetLastValueChangeWasInteractive(false);
+ SetUserInteracted(false);
+
+ switch (GetValueMode()) {
+ case VALUE_MODE_VALUE: {
+ nsresult result = SetDefaultValueAsValue();
+ if (CreatesDateTimeWidget()) {
+ // mFocusedValue has to be set here, so that `FireChangeEventIfNeeded`
+ // can fire a change event if necessary.
+ GetValue(mFocusedValue, CallerType::System);
+ }
+ return result;
+ }
+ case VALUE_MODE_DEFAULT_ON:
+ DoSetChecked(DefaultChecked(), true, false);
+ return NS_OK;
+ case VALUE_MODE_FILENAME:
+ ClearFiles(false);
+ return NS_OK;
+ case VALUE_MODE_DEFAULT:
+ default:
+ return NS_OK;
+ }
+}
+
+NS_IMETHODIMP
+HTMLInputElement::SubmitNamesValues(FormData* aFormData) {
+ // For type=reset, and type=button, we just never submit, period.
+ // For type=image and type=button, we only submit if we were the button
+ // pressed
+ // For type=radio and type=checkbox, we only submit if checked=true
+ if (mType == FormControlType::InputReset ||
+ mType == FormControlType::InputButton ||
+ ((mType == FormControlType::InputSubmit ||
+ mType == FormControlType::InputImage) &&
+ aFormData->GetSubmitterElement() != this) ||
+ ((mType == FormControlType::InputRadio ||
+ mType == FormControlType::InputCheckbox) &&
+ !mChecked)) {
+ return NS_OK;
+ }
+
+ // Get the name
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ // Submit .x, .y for input type=image
+ if (mType == FormControlType::InputImage) {
+ // Get a property set by the frame to find out where it was clicked.
+ const auto* lastClickedPoint =
+ static_cast<CSSIntPoint*>(GetProperty(nsGkAtoms::imageClickedPoint));
+ int32_t x, y;
+ if (lastClickedPoint) {
+ // Convert the values to strings for submission
+ x = lastClickedPoint->x;
+ y = lastClickedPoint->y;
+ } else {
+ x = y = 0;
+ }
+
+ nsAutoString xVal, yVal;
+ xVal.AppendInt(x);
+ yVal.AppendInt(y);
+
+ if (!name.IsEmpty()) {
+ aFormData->AddNameValuePair(name + u".x"_ns, xVal);
+ aFormData->AddNameValuePair(name + u".y"_ns, yVal);
+ } else {
+ // If the Image Element has no name, simply return x and y
+ // to Nav and IE compatibility.
+ aFormData->AddNameValuePair(u"x"_ns, xVal);
+ aFormData->AddNameValuePair(u"y"_ns, yVal);
+ }
+
+ return NS_OK;
+ }
+
+ // If name not there, don't submit
+ if (name.IsEmpty()) {
+ return NS_OK;
+ }
+
+ //
+ // Submit file if its input type=file and this encoding method accepts files
+ //
+ if (mType == FormControlType::InputFile) {
+ // Submit files
+
+ const nsTArray<OwningFileOrDirectory>& files =
+ GetFilesOrDirectoriesInternal();
+
+ if (files.IsEmpty()) {
+ NS_ENSURE_STATE(GetOwnerGlobal());
+ ErrorResult rv;
+ RefPtr<Blob> blob = Blob::CreateStringBlob(
+ GetOwnerGlobal(), ""_ns, u"application/octet-stream"_ns);
+ RefPtr<File> file = blob->ToFile(u""_ns, rv);
+
+ if (!rv.Failed()) {
+ aFormData->AddNameBlobPair(name, file);
+ }
+
+ return rv.StealNSResult();
+ }
+
+ for (uint32_t i = 0; i < files.Length(); ++i) {
+ if (files[i].IsFile()) {
+ aFormData->AddNameBlobPair(name, files[i].GetAsFile());
+ } else {
+ MOZ_ASSERT(files[i].IsDirectory());
+ aFormData->AddNameDirectoryPair(name, files[i].GetAsDirectory());
+ }
+ }
+
+ return NS_OK;
+ }
+
+ if (mType == FormControlType::InputHidden &&
+ name.LowerCaseEqualsLiteral("_charset_")) {
+ nsCString charset;
+ aFormData->GetCharset(charset);
+ return aFormData->AddNameValuePair(name, NS_ConvertASCIItoUTF16(charset));
+ }
+
+ //
+ // Submit name=value
+ //
+
+ // Get the value
+ nsAutoString value;
+ GetValue(value, CallerType::System);
+
+ if (mType == FormControlType::InputSubmit && value.IsEmpty() &&
+ !HasAttr(nsGkAtoms::value)) {
+ // Get our default value, which is the same as our default label
+ nsAutoString defaultValue;
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "Submit", OwnerDoc(), defaultValue);
+ value = defaultValue;
+ }
+
+ const nsresult rv = aFormData->AddNameValuePair(name, value);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Submit dirname=dir
+ if (IsAutoDirectionalityAssociated()) {
+ return SubmitDirnameDir(aFormData);
+ }
+
+ return NS_OK;
+}
+
+static nsTArray<FileContentData> SaveFileContentData(
+ const nsTArray<OwningFileOrDirectory>& aArray) {
+ nsTArray<FileContentData> res(aArray.Length());
+ for (const auto& it : aArray) {
+ if (it.IsFile()) {
+ RefPtr<BlobImpl> impl = it.GetAsFile()->Impl();
+ res.AppendElement(std::move(impl));
+ } else {
+ MOZ_ASSERT(it.IsDirectory());
+ nsString fullPath;
+ nsresult rv = it.GetAsDirectory()->GetFullRealPath(fullPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ res.AppendElement(std::move(fullPath));
+ }
+ }
+ return res;
+}
+
+void HTMLInputElement::SaveState() {
+ PresState* state = nullptr;
+ switch (GetValueMode()) {
+ case VALUE_MODE_DEFAULT_ON:
+ if (mCheckedChanged) {
+ state = GetPrimaryPresState();
+ if (!state) {
+ return;
+ }
+
+ state->contentData() = CheckedContentData(mChecked);
+ }
+ break;
+ case VALUE_MODE_FILENAME:
+ if (!mFileData->mFilesOrDirectories.IsEmpty()) {
+ state = GetPrimaryPresState();
+ if (!state) {
+ return;
+ }
+
+ state->contentData() =
+ SaveFileContentData(mFileData->mFilesOrDirectories);
+ }
+ break;
+ case VALUE_MODE_VALUE:
+ case VALUE_MODE_DEFAULT:
+ // VALUE_MODE_DEFAULT shouldn't have their value saved except 'hidden',
+ // mType should have never been FormControlType::InputPassword and value
+ // should have changed.
+ if ((GetValueMode() == VALUE_MODE_DEFAULT &&
+ mType != FormControlType::InputHidden) ||
+ mHasBeenTypePassword || !mValueChanged) {
+ break;
+ }
+
+ state = GetPrimaryPresState();
+ if (!state) {
+ return;
+ }
+
+ nsAutoString value;
+ GetValue(value, CallerType::System);
+
+ if (!IsSingleLineTextControl(false) &&
+ NS_FAILED(nsLinebreakConverter::ConvertStringLineBreaks(
+ value, nsLinebreakConverter::eLinebreakPlatform,
+ nsLinebreakConverter::eLinebreakContent))) {
+ NS_ERROR("Converting linebreaks failed!");
+ return;
+ }
+
+ state->contentData() =
+ TextContentData(value, mLastValueChangeWasInteractive);
+ break;
+ }
+
+ if (mDisabledChanged) {
+ if (!state) {
+ state = GetPrimaryPresState();
+ }
+ if (state) {
+ // We do not want to save the real disabled state but the disabled
+ // attribute.
+ state->disabled() = HasAttr(nsGkAtoms::disabled);
+ state->disabledSet() = true;
+ }
+ }
+}
+
+void HTMLInputElement::DoneCreatingElement() {
+ mDoneCreating = true;
+
+ //
+ // Restore state as needed. Note that disabled state applies to all control
+ // types.
+ //
+ bool restoredCheckedState = false;
+ if (!mInhibitRestoration) {
+ GenerateStateKey();
+ restoredCheckedState = RestoreFormControlState();
+ }
+
+ //
+ // If restore does not occur, we initialize .checked using the CHECKED
+ // property.
+ //
+ if (!restoredCheckedState && mShouldInitChecked) {
+ DoSetChecked(DefaultChecked(), false, false);
+ }
+
+ // Sanitize the value and potentially set mFocusedValue.
+ if (GetValueMode() == VALUE_MODE_VALUE) {
+ nsAutoString value;
+ GetValue(value, CallerType::System);
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // may potentially be big, but most likely we've failed to allocate
+ // before the type change.)
+ SetValueInternal(value, ValueSetterOption::ByInternalAPI);
+
+ if (CreatesDateTimeWidget()) {
+ // mFocusedValue has to be set here, so that `FireChangeEventIfNeeded` can
+ // fire a change event if necessary.
+ mFocusedValue = value;
+ }
+ }
+
+ mShouldInitChecked = false;
+}
+
+void HTMLInputElement::DestroyContent() {
+ nsImageLoadingContent::Destroy();
+ TextControlElement::DestroyContent();
+}
+
+void HTMLInputElement::UpdateValidityElementStates(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::VALIDITY_STATES);
+ if (!IsCandidateForConstraintValidation()) {
+ return;
+ }
+ ElementState state;
+ if (IsValid()) {
+ state |= ElementState::VALID;
+ if (mUserInteracted) {
+ state |= ElementState::USER_VALID;
+ }
+ } else {
+ state |= ElementState::INVALID;
+ if (mUserInteracted) {
+ state |= ElementState::USER_INVALID;
+ }
+ }
+ AddStatesSilently(state);
+}
+
+static nsTArray<OwningFileOrDirectory> RestoreFileContentData(
+ nsPIDOMWindowInner* aWindow, const nsTArray<FileContentData>& aData) {
+ nsTArray<OwningFileOrDirectory> res(aData.Length());
+ for (const auto& it : aData) {
+ if (it.type() == FileContentData::TBlobImpl) {
+ if (!it.get_BlobImpl()) {
+ // Serialization failed, skip this file.
+ continue;
+ }
+
+ RefPtr<File> file = File::Create(aWindow->AsGlobal(), it.get_BlobImpl());
+ if (NS_WARN_IF(!file)) {
+ continue;
+ }
+
+ OwningFileOrDirectory* element = res.AppendElement();
+ element->SetAsFile() = file;
+ } else {
+ MOZ_ASSERT(it.type() == FileContentData::TnsString);
+ nsCOMPtr<nsIFile> file;
+ nsresult rv =
+ NS_NewLocalFile(it.get_nsString(), true, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ RefPtr<Directory> directory =
+ Directory::Create(aWindow->AsGlobal(), file);
+ MOZ_ASSERT(directory);
+
+ OwningFileOrDirectory* element = res.AppendElement();
+ element->SetAsDirectory() = directory;
+ }
+ }
+ return res;
+}
+
+bool HTMLInputElement::RestoreState(PresState* aState) {
+ bool restoredCheckedState = false;
+
+ const PresContentData& inputState = aState->contentData();
+
+ switch (GetValueMode()) {
+ case VALUE_MODE_DEFAULT_ON:
+ if (inputState.type() == PresContentData::TCheckedContentData) {
+ restoredCheckedState = true;
+ bool checked = inputState.get_CheckedContentData().checked();
+ DoSetChecked(checked, true, true);
+ }
+ break;
+ case VALUE_MODE_FILENAME:
+ if (inputState.type() == PresContentData::TArrayOfFileContentData) {
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (window) {
+ nsTArray<OwningFileOrDirectory> array =
+ RestoreFileContentData(window, inputState);
+ SetFilesOrDirectories(array, true);
+ }
+ }
+ break;
+ case VALUE_MODE_VALUE:
+ case VALUE_MODE_DEFAULT:
+ if (GetValueMode() == VALUE_MODE_DEFAULT &&
+ mType != FormControlType::InputHidden) {
+ break;
+ }
+
+ if (inputState.type() == PresContentData::TTextContentData) {
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // may potentially be big, but most likely we've failed to allocate
+ // before the type change.)
+ SetValueInternal(inputState.get_TextContentData().value(),
+ ValueSetterOption::SetValueChanged);
+ if (inputState.get_TextContentData().lastValueChangeWasInteractive()) {
+ SetLastValueChangeWasInteractive(true);
+ }
+ }
+ break;
+ }
+
+ if (aState->disabledSet() && !aState->disabled()) {
+ SetDisabled(false, IgnoreErrors());
+ }
+
+ return restoredCheckedState;
+}
+
+/*
+ * Radio group stuff
+ */
+
+void HTMLInputElement::AddToRadioGroup() {
+ MOZ_ASSERT(!mRadioGroupContainer,
+ "Radio button must be removed from previous radio group container "
+ "before being added to another!");
+
+ // If the element has no radio group container we can stop here.
+ auto* container = FindTreeRadioGroupContainer();
+ if (!container) {
+ return;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ // If we are part of a radio group, the element must have a name.
+ MOZ_ASSERT(!name.IsEmpty());
+
+ //
+ // Add the radio to the radio group container.
+ //
+ container->AddToRadioGroup(name, this, mForm);
+ mRadioGroupContainer = container;
+
+ //
+ // If the input element is checked, and we add it to the group, it will
+ // deselect whatever is currently selected in that group
+ //
+ if (mChecked) {
+ //
+ // If it is checked, call "RadioSetChecked" to perform the selection/
+ // deselection ritual. This has the side effect of repainting the
+ // radio button, but as adding a checked radio button into the group
+ // should not be that common an occurrence, I think we can live with
+ // that.
+ // Make sure not to notify if we're still being created.
+ //
+ RadioSetChecked(mDoneCreating);
+ } else {
+ bool indeterminate = !container->GetCurrentRadioButton(name);
+ SetStates(ElementState::INDETERMINATE, indeterminate, mDoneCreating);
+ }
+
+ //
+ // For integrity purposes, we have to ensure that "checkedChanged" is
+ // the same for this new element as for all the others in the group
+ //
+ bool checkedChanged = mCheckedChanged;
+
+ nsCOMPtr<nsIRadioVisitor> visitor =
+ new nsRadioGetCheckedChangedVisitor(&checkedChanged, this);
+ VisitGroup(visitor);
+
+ SetCheckedChangedInternal(checkedChanged);
+
+ // We initialize the validity of the element to the validity of the group
+ // because we assume UpdateValueMissingState() will be called after.
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING,
+ container->GetValueMissingState(name));
+}
+
+void HTMLInputElement::RemoveFromRadioGroup() {
+ auto* container = GetCurrentRadioGroupContainer();
+ if (!container) {
+ return;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ // If this button was checked, we need to notify the group that there is no
+ // longer a selected radio button
+ if (mChecked) {
+ container->SetCurrentRadioButton(name, nullptr);
+ nsCOMPtr<nsIRadioVisitor> visitor = new nsRadioUpdateStateVisitor(this);
+ VisitGroup(visitor);
+ } else {
+ AddStates(ElementState::INDETERMINATE);
+ }
+
+ // Remove this radio from its group in the container.
+ // We need to call UpdateValueMissingValidityStateForRadio before to make sure
+ // the group validity is updated (with this element being ignored).
+ UpdateValueMissingValidityStateForRadio(true);
+ container->RemoveFromRadioGroup(name, this);
+ mRadioGroupContainer = nullptr;
+}
+
+bool HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable(
+ aWithMouse, aIsFocusable, aTabIndex)) {
+ return true;
+ }
+
+ if (IsDisabled()) {
+ *aIsFocusable = false;
+ return true;
+ }
+
+ if (IsSingleLineTextControl(false) || mType == FormControlType::InputRange) {
+ *aIsFocusable = true;
+ return false;
+ }
+
+ const bool defaultFocusable = IsFormControlDefaultFocusable(aWithMouse);
+ if (CreatesDateTimeWidget()) {
+ if (aTabIndex) {
+ // We only want our native anonymous child to be tabable to, not ourself.
+ *aTabIndex = -1;
+ }
+ *aIsFocusable = true;
+ return true;
+ }
+
+ if (mType == FormControlType::InputHidden) {
+ if (aTabIndex) {
+ *aTabIndex = -1;
+ }
+ *aIsFocusable = false;
+ return false;
+ }
+
+ if (!aTabIndex) {
+ // The other controls are all focusable
+ *aIsFocusable = defaultFocusable;
+ return false;
+ }
+
+ if (mType != FormControlType::InputRadio) {
+ *aIsFocusable = defaultFocusable;
+ return false;
+ }
+
+ if (mChecked) {
+ // Selected radio buttons are tabbable
+ *aIsFocusable = defaultFocusable;
+ return false;
+ }
+
+ // Current radio button is not selected.
+ // But make it tabbable if nothing in group is selected.
+ auto* container = GetCurrentRadioGroupContainer();
+ if (!container) {
+ *aIsFocusable = defaultFocusable;
+ return false;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ if (container->GetCurrentRadioButton(name)) {
+ *aTabIndex = -1;
+ }
+ *aIsFocusable = defaultFocusable;
+ return false;
+}
+
+nsresult HTMLInputElement::VisitGroup(nsIRadioVisitor* aVisitor) {
+ if (auto* container = GetCurrentRadioGroupContainer()) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ return container->WalkRadioGroup(name, aVisitor);
+ }
+
+ aVisitor->Visit(this);
+ return NS_OK;
+}
+
+HTMLInputElement::ValueModeType HTMLInputElement::GetValueMode() const {
+ switch (mType) {
+ case FormControlType::InputHidden:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputButton:
+ case FormControlType::InputReset:
+ case FormControlType::InputImage:
+ return VALUE_MODE_DEFAULT;
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRadio:
+ return VALUE_MODE_DEFAULT_ON;
+ case FormControlType::InputFile:
+ return VALUE_MODE_FILENAME;
+#ifdef DEBUG
+ case FormControlType::InputText:
+ case FormControlType::InputPassword:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputUrl:
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputColor:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return VALUE_MODE_VALUE;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected input type in GetValueMode()");
+ return VALUE_MODE_VALUE;
+#else // DEBUG
+ default:
+ return VALUE_MODE_VALUE;
+#endif // DEBUG
+ }
+}
+
+bool HTMLInputElement::IsMutable() const {
+ return !IsDisabled() &&
+ !(DoesReadOnlyApply() && State().HasState(ElementState::READONLY));
+}
+
+bool HTMLInputElement::DoesRequiredApply() const {
+ switch (mType) {
+ case FormControlType::InputHidden:
+ case FormControlType::InputButton:
+ case FormControlType::InputImage:
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputRange:
+ case FormControlType::InputColor:
+ return false;
+#ifdef DEBUG
+ case FormControlType::InputRadio:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputFile:
+ case FormControlType::InputText:
+ case FormControlType::InputPassword:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputUrl:
+ case FormControlType::InputNumber:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesRequiredApply()");
+ return true;
+#else // DEBUG
+ default:
+ return true;
+#endif // DEBUG
+ }
+}
+
+bool HTMLInputElement::PlaceholderApplies() const {
+ if (IsDateTimeInputType(mType)) {
+ return false;
+ }
+ return IsSingleLineTextControl(false);
+}
+
+bool HTMLInputElement::DoesMinMaxApply() const {
+ switch (mType) {
+ case FormControlType::InputNumber:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputRange:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+#ifdef DEBUG
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ case FormControlType::InputButton:
+ case FormControlType::InputHidden:
+ case FormControlType::InputRadio:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputFile:
+ case FormControlType::InputText:
+ case FormControlType::InputPassword:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputUrl:
+ case FormControlType::InputColor:
+ return false;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesRequiredApply()");
+ return false;
+#else // DEBUG
+ default:
+ return false;
+#endif // DEBUG
+ }
+}
+
+bool HTMLInputElement::DoesAutocompleteApply() const {
+ switch (mType) {
+ case FormControlType::InputHidden:
+ case FormControlType::InputText:
+ case FormControlType::InputSearch:
+ case FormControlType::InputUrl:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputPassword:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ case FormControlType::InputColor:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+#ifdef DEBUG
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ case FormControlType::InputButton:
+ case FormControlType::InputRadio:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputFile:
+ return false;
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Unexpected input type in DoesAutocompleteApply()");
+ return false;
+#else // DEBUG
+ default:
+ return false;
+#endif // DEBUG
+ }
+}
+
+Decimal HTMLInputElement::GetStep() const {
+ MOZ_ASSERT(DoesStepApply(), "GetStep() can only be called if @step applies");
+
+ if (!HasAttr(nsGkAtoms::step)) {
+ return GetDefaultStep() * GetStepScaleFactor();
+ }
+
+ nsAutoString stepStr;
+ GetAttr(nsGkAtoms::step, stepStr);
+
+ if (stepStr.LowerCaseEqualsLiteral("any")) {
+ // The element can't suffer from step mismatch if there is no step.
+ return kStepAny;
+ }
+
+ Decimal step = StringToDecimal(stepStr);
+ if (!step.isFinite() || step <= Decimal(0)) {
+ step = GetDefaultStep();
+ }
+
+ // For input type=date, we round the step value to have a rounded day.
+ if (mType == FormControlType::InputDate ||
+ mType == FormControlType::InputMonth ||
+ mType == FormControlType::InputWeek) {
+ step = std::max(step.round(), Decimal(1));
+ }
+
+ return step * GetStepScaleFactor();
+}
+
+// ConstraintValidation
+
+void HTMLInputElement::SetCustomValidity(const nsAString& aError) {
+ ConstraintValidation::SetCustomValidity(aError);
+ UpdateValidityElementStates(true);
+}
+
+bool HTMLInputElement::IsTooLong() {
+ if (!mValueChanged || !mLastValueChangeWasInteractive) {
+ return false;
+ }
+
+ return mInputType->IsTooLong();
+}
+
+bool HTMLInputElement::IsTooShort() {
+ if (!mValueChanged || !mLastValueChangeWasInteractive) {
+ return false;
+ }
+
+ return mInputType->IsTooShort();
+}
+
+bool HTMLInputElement::IsValueMissing() const {
+ // Should use UpdateValueMissingValidityStateForRadio() for type radio.
+ MOZ_ASSERT(mType != FormControlType::InputRadio);
+
+ return mInputType->IsValueMissing();
+}
+
+bool HTMLInputElement::HasTypeMismatch() const {
+ return mInputType->HasTypeMismatch();
+}
+
+Maybe<bool> HTMLInputElement::HasPatternMismatch() const {
+ return mInputType->HasPatternMismatch();
+}
+
+bool HTMLInputElement::IsRangeOverflow() const {
+ return mInputType->IsRangeOverflow();
+}
+
+bool HTMLInputElement::IsRangeUnderflow() const {
+ return mInputType->IsRangeUnderflow();
+}
+
+bool HTMLInputElement::ValueIsStepMismatch(const Decimal& aValue) const {
+ if (aValue.isNaN()) {
+ // The element can't suffer from step mismatch if its value isn't a
+ // number.
+ return false;
+ }
+
+ Decimal step = GetStep();
+ if (step == kStepAny) {
+ return false;
+ }
+
+ // Value has to be an integral multiple of step.
+ return NS_floorModulo(aValue - GetStepBase(), step) != Decimal(0);
+}
+
+bool HTMLInputElement::HasStepMismatch() const {
+ return mInputType->HasStepMismatch();
+}
+
+bool HTMLInputElement::HasBadInput() const { return mInputType->HasBadInput(); }
+
+void HTMLInputElement::UpdateTooLongValidityState() {
+ SetValidityState(VALIDITY_STATE_TOO_LONG, IsTooLong());
+}
+
+void HTMLInputElement::UpdateTooShortValidityState() {
+ SetValidityState(VALIDITY_STATE_TOO_SHORT, IsTooShort());
+}
+
+void HTMLInputElement::UpdateValueMissingValidityStateForRadio(
+ bool aIgnoreSelf) {
+ MOZ_ASSERT(mType == FormControlType::InputRadio,
+ "This should be called only for radio input types");
+
+ HTMLInputElement* selection = GetSelectedRadioButton();
+
+ // If there is no selection, that might mean the radio is not in a group.
+ // In that case, we can look for the checked state of the radio.
+ bool selected = selection || (!aIgnoreSelf && mChecked);
+ bool required = !aIgnoreSelf && IsRequired();
+
+ auto* container = GetCurrentRadioGroupContainer();
+ if (!container) {
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, false);
+ return;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ // If the current radio is required and not ignored, we can assume the entire
+ // group is required.
+ if (!required) {
+ required = (aIgnoreSelf && IsRequired())
+ ? container->GetRequiredRadioCount(name) - 1
+ : container->GetRequiredRadioCount(name);
+ }
+
+ bool valueMissing = required && !selected;
+ if (container->GetValueMissingState(name) != valueMissing) {
+ container->SetValueMissingState(name, valueMissing);
+
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, valueMissing);
+
+ // nsRadioSetValueMissingState will call ElementStateChanged while visiting.
+ nsAutoScriptBlocker scriptBlocker;
+ nsCOMPtr<nsIRadioVisitor> visitor =
+ new nsRadioSetValueMissingState(this, valueMissing);
+ VisitGroup(visitor);
+ }
+}
+
+void HTMLInputElement::UpdateValueMissingValidityState() {
+ if (mType == FormControlType::InputRadio) {
+ UpdateValueMissingValidityStateForRadio(false);
+ return;
+ }
+
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsValueMissing());
+}
+
+void HTMLInputElement::UpdateTypeMismatchValidityState() {
+ SetValidityState(VALIDITY_STATE_TYPE_MISMATCH, HasTypeMismatch());
+}
+
+void HTMLInputElement::UpdatePatternMismatchValidityState() {
+ Maybe<bool> hasMismatch = HasPatternMismatch();
+ // Don't update if the JS engine failed to evaluate it.
+ if (hasMismatch.isSome()) {
+ SetValidityState(VALIDITY_STATE_PATTERN_MISMATCH, hasMismatch.value());
+ }
+}
+
+void HTMLInputElement::UpdateRangeOverflowValidityState() {
+ SetValidityState(VALIDITY_STATE_RANGE_OVERFLOW, IsRangeOverflow());
+ UpdateInRange(true);
+}
+
+void HTMLInputElement::UpdateRangeUnderflowValidityState() {
+ SetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW, IsRangeUnderflow());
+ UpdateInRange(true);
+}
+
+void HTMLInputElement::UpdateStepMismatchValidityState() {
+ SetValidityState(VALIDITY_STATE_STEP_MISMATCH, HasStepMismatch());
+}
+
+void HTMLInputElement::UpdateBadInputValidityState() {
+ SetValidityState(VALIDITY_STATE_BAD_INPUT, HasBadInput());
+}
+
+void HTMLInputElement::UpdateAllValidityStates(bool aNotify) {
+ bool validBefore = IsValid();
+ UpdateAllValidityStatesButNotElementState();
+ if (validBefore != IsValid()) {
+ UpdateValidityElementStates(aNotify);
+ }
+}
+
+void HTMLInputElement::UpdateAllValidityStatesButNotElementState() {
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ UpdateValueMissingValidityState();
+ UpdateTypeMismatchValidityState();
+ UpdatePatternMismatchValidityState();
+ UpdateRangeOverflowValidityState();
+ UpdateRangeUnderflowValidityState();
+ UpdateStepMismatchValidityState();
+ UpdateBadInputValidityState();
+}
+
+void HTMLInputElement::UpdateBarredFromConstraintValidation() {
+ // NOTE: readonly attribute causes an element to be barred from constraint
+ // validation even if it doesn't apply to that input type. That's rather
+ // weird, but pre-existing behavior.
+ bool wasCandidate = IsCandidateForConstraintValidation();
+ SetBarredFromConstraintValidation(
+ mType == FormControlType::InputHidden ||
+ mType == FormControlType::InputButton ||
+ mType == FormControlType::InputReset || IsDisabled() ||
+ HasAttr(nsGkAtoms::readonly) ||
+ HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR));
+ if (IsCandidateForConstraintValidation() != wasCandidate) {
+ UpdateInRange(true);
+ }
+}
+
+nsresult HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage,
+ ValidityStateType aType) {
+ return mInputType->GetValidationMessage(aValidationMessage, aType);
+}
+
+bool HTMLInputElement::IsSingleLineTextControl() const {
+ return IsSingleLineTextControl(false);
+}
+
+bool HTMLInputElement::IsTextArea() const { return false; }
+
+bool HTMLInputElement::IsPasswordTextControl() const {
+ return mType == FormControlType::InputPassword;
+}
+
+int32_t HTMLInputElement::GetCols() {
+ // Else we know (assume) it is an input with size attr
+ const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::size);
+ if (attr && attr->Type() == nsAttrValue::eInteger) {
+ int32_t cols = attr->GetIntegerValue();
+ if (cols > 0) {
+ return cols;
+ }
+ }
+
+ return DEFAULT_COLS;
+}
+
+int32_t HTMLInputElement::GetWrapCols() {
+ return 0; // only textarea's can have wrap cols
+}
+
+int32_t HTMLInputElement::GetRows() { return DEFAULT_ROWS; }
+
+void HTMLInputElement::GetDefaultValueFromContent(nsAString& aValue,
+ bool aForDisplay) {
+ if (!GetEditorState()) {
+ return;
+ }
+ GetDefaultValue(aValue);
+ // This is called by the frame to show the value.
+ // We have to sanitize it when needed.
+ // FIXME: Do we want to sanitize even when aForDisplay is false?
+ if (mDoneCreating) {
+ SanitizeValue(aValue, aForDisplay ? SanitizationKind::ForDisplay
+ : SanitizationKind::ForValueGetter);
+ }
+}
+
+bool HTMLInputElement::ValueChanged() const { return mValueChanged; }
+
+void HTMLInputElement::GetTextEditorValue(nsAString& aValue) const {
+ if (TextControlState* state = GetEditorState()) {
+ state->GetValue(aValue, /* aIgnoreWrap = */ true, /* aForDisplay = */ true);
+ }
+}
+
+void HTMLInputElement::InitializeKeyboardEventListeners() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ state->InitializeKeyboardEventListeners();
+ }
+}
+
+void HTMLInputElement::UpdatePlaceholderShownState() {
+ SetStates(ElementState::PLACEHOLDER_SHOWN,
+ IsValueEmpty() && PlaceholderApplies() &&
+ HasAttr(nsGkAtoms::placeholder));
+}
+
+void HTMLInputElement::OnValueChanged(ValueChangeKind aKind,
+ bool aNewValueEmpty,
+ const nsAString* aKnownNewValue) {
+ MOZ_ASSERT_IF(aKnownNewValue, aKnownNewValue->IsEmpty() == aNewValueEmpty);
+ if (aKind != ValueChangeKind::Internal) {
+ mLastValueChangeWasInteractive = aKind == ValueChangeKind::UserInteraction;
+ }
+
+ if (aNewValueEmpty != IsValueEmpty()) {
+ SetStates(ElementState::VALUE_EMPTY, aNewValueEmpty);
+ UpdatePlaceholderShownState();
+ }
+
+ UpdateAllValidityStates(true);
+
+ if (HasDirAuto()) {
+ SetAutoDirectionality(true, aKnownNewValue);
+ }
+}
+
+bool HTMLInputElement::HasCachedSelection() {
+ TextControlState* state = GetEditorState();
+ if (!state) {
+ return false;
+ }
+ return state->IsSelectionCached() && state->HasNeverInitializedBefore() &&
+ state->GetSelectionProperties().GetStart() !=
+ state->GetSelectionProperties().GetEnd();
+}
+
+void HTMLInputElement::SetRevealPassword(bool aValue) {
+ if (NS_WARN_IF(mType != FormControlType::InputPassword)) {
+ return;
+ }
+ if (aValue == State().HasState(ElementState::REVEALED)) {
+ return;
+ }
+ RefPtr doc = OwnerDoc();
+ // We allow chrome code to prevent this. This is important for about:logins,
+ // which may need to run some OS-dependent authentication code before
+ // revealing the saved passwords.
+ bool defaultAction = true;
+ nsContentUtils::DispatchEventOnlyToChrome(
+ doc, this, u"MozWillToggleReveal"_ns, CanBubble::eYes, Cancelable::eYes,
+ &defaultAction);
+ if (NS_WARN_IF(!defaultAction)) {
+ return;
+ }
+ SetStates(ElementState::REVEALED, aValue);
+}
+
+bool HTMLInputElement::RevealPassword() const {
+ if (NS_WARN_IF(mType != FormControlType::InputPassword)) {
+ return false;
+ }
+ return State().HasState(ElementState::REVEALED);
+}
+
+void HTMLInputElement::FieldSetDisabledChanged(bool aNotify) {
+ // This *has* to be called *before* UpdateBarredFromConstraintValidation and
+ // UpdateValueMissingValidityState because these two functions depend on our
+ // disabled state.
+ nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify);
+
+ UpdateValueMissingValidityState();
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(aNotify);
+}
+
+void HTMLInputElement::SetFilePickerFiltersFromAccept(
+ nsIFilePicker* filePicker) {
+ // We always add |filterAll|
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+
+ NS_ASSERTION(HasAttr(nsGkAtoms::accept),
+ "You should not call SetFilePickerFiltersFromAccept if the"
+ " element has no accept attribute!");
+
+ // Services to retrieve image/*, audio/*, video/* filters
+ nsCOMPtr<nsIStringBundleService> stringService =
+ components::StringBundle::Service();
+ if (!stringService) {
+ return;
+ }
+ nsCOMPtr<nsIStringBundle> filterBundle;
+ if (NS_FAILED(stringService->CreateBundle(
+ "chrome://global/content/filepicker.properties",
+ getter_AddRefs(filterBundle)))) {
+ return;
+ }
+
+ // Service to retrieve mime type information for mime types filters
+ nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
+ if (!mimeService) {
+ return;
+ }
+
+ nsAutoString accept;
+ GetAttr(nsGkAtoms::accept, accept);
+
+ HTMLSplitOnSpacesTokenizer tokenizer(accept, ',');
+
+ nsTArray<nsFilePickerFilter> filters;
+ nsString allExtensionsList;
+
+ // Retrieve all filters
+ while (tokenizer.hasMoreTokens()) {
+ const nsDependentSubstring& token = tokenizer.nextToken();
+
+ if (token.IsEmpty()) {
+ continue;
+ }
+
+ int32_t filterMask = 0;
+ nsString filterName;
+ nsString extensionListStr;
+
+ // First, check for image/audio/video filters...
+ if (token.EqualsLiteral("image/*")) {
+ filterMask = nsIFilePicker::filterImages;
+ filterBundle->GetStringFromName("imageFilter", extensionListStr);
+ } else if (token.EqualsLiteral("audio/*")) {
+ filterMask = nsIFilePicker::filterAudio;
+ filterBundle->GetStringFromName("audioFilter", extensionListStr);
+ } else if (token.EqualsLiteral("video/*")) {
+ filterMask = nsIFilePicker::filterVideo;
+ filterBundle->GetStringFromName("videoFilter", extensionListStr);
+ } else if (token.First() == '.') {
+ if (token.Contains(';') || token.Contains('*')) {
+ // Ignore this filter as it contains reserved characters
+ continue;
+ }
+ extensionListStr = u"*"_ns + token;
+ filterName = extensionListStr;
+ } else {
+ //... if no image/audio/video filter is found, check mime types filters
+ nsCOMPtr<nsIMIMEInfo> mimeInfo;
+ if (NS_FAILED(
+ mimeService->GetFromTypeAndExtension(NS_ConvertUTF16toUTF8(token),
+ ""_ns, // No extension
+ getter_AddRefs(mimeInfo))) ||
+ !mimeInfo) {
+ continue;
+ }
+
+ // Get a name for the filter: first try the description, then the mime
+ // type name if there is no description
+ mimeInfo->GetDescription(filterName);
+ if (filterName.IsEmpty()) {
+ nsCString mimeTypeName;
+ mimeInfo->GetType(mimeTypeName);
+ CopyUTF8toUTF16(mimeTypeName, filterName);
+ }
+
+ // Get extension list
+ nsCOMPtr<nsIUTF8StringEnumerator> extensions;
+ mimeInfo->GetFileExtensions(getter_AddRefs(extensions));
+
+ bool hasMore;
+ while (NS_SUCCEEDED(extensions->HasMore(&hasMore)) && hasMore) {
+ nsCString extension;
+ if (NS_FAILED(extensions->GetNext(extension))) {
+ continue;
+ }
+ if (!extensionListStr.IsEmpty()) {
+ extensionListStr.AppendLiteral("; ");
+ }
+ extensionListStr += u"*."_ns + NS_ConvertUTF8toUTF16(extension);
+ }
+ }
+
+ if (!filterMask && (extensionListStr.IsEmpty() || filterName.IsEmpty())) {
+ // No valid filter found
+ continue;
+ }
+
+ // At this point we're sure the token represents a valid filter, so pass
+ // it directly as a raw filter.
+ filePicker->AppendRawFilter(token);
+
+ // If we arrived here, that means we have a valid filter: let's create it
+ // and add it to our list, if no similar filter is already present
+ nsFilePickerFilter filter;
+ if (filterMask) {
+ filter = nsFilePickerFilter(filterMask);
+ } else {
+ filter = nsFilePickerFilter(filterName, extensionListStr);
+ }
+
+ if (!filters.Contains(filter)) {
+ if (!allExtensionsList.IsEmpty()) {
+ allExtensionsList.AppendLiteral("; ");
+ }
+ allExtensionsList += extensionListStr;
+ filters.AppendElement(filter);
+ }
+ }
+
+ // Remove similar filters
+ // Iterate over a copy, as we might modify the original filters list
+ const nsTArray<nsFilePickerFilter> filtersCopy = filters.Clone();
+ for (uint32_t i = 0; i < filtersCopy.Length(); ++i) {
+ const nsFilePickerFilter& filterToCheck = filtersCopy[i];
+ if (filterToCheck.mFilterMask) {
+ continue;
+ }
+ for (uint32_t j = 0; j < filtersCopy.Length(); ++j) {
+ if (i == j) {
+ continue;
+ }
+ // Check if this filter's extension list is a substring of the other one.
+ // e.g. if filters are "*.jpeg" and "*.jpeg; *.jpg" the first one should
+ // be removed.
+ // Add an extra "; " to be sure the check will work and avoid cases like
+ // "*.xls" being a subtring of "*.xslx" while those are two differents
+ // filters and none should be removed.
+ if (FindInReadable(filterToCheck.mFilter + u";"_ns,
+ filtersCopy[j].mFilter + u";"_ns)) {
+ // We already have a similar, less restrictive filter (i.e.
+ // filterToCheck extensionList is just a subset of another filter
+ // extension list): remove this one
+ filters.RemoveElement(filterToCheck);
+ }
+ }
+ }
+
+ // Add "All Supported Types" filter
+ if (filters.Length() > 1) {
+ nsAutoString title;
+ nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "AllSupportedTypes", title);
+ filePicker->AppendFilter(title, allExtensionsList);
+ }
+
+ // Add each filter
+ for (uint32_t i = 0; i < filters.Length(); ++i) {
+ const nsFilePickerFilter& filter = filters[i];
+ if (filter.mFilterMask) {
+ filePicker->AppendFilters(filter.mFilterMask);
+ } else {
+ filePicker->AppendFilter(filter.mTitle, filter.mFilter);
+ }
+ }
+
+ if (filters.Length() >= 1) {
+ // |filterAll| will always use index=0 so we need to set index=1 as the
+ // current filter. This will be "All Supported Types" for multiple filters.
+ filePicker->SetFilterIndex(1);
+ }
+}
+
+Decimal HTMLInputElement::GetStepScaleFactor() const {
+ MOZ_ASSERT(DoesStepApply());
+
+ switch (mType) {
+ case FormControlType::InputDate:
+ return kStepScaleFactorDate;
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ return kStepScaleFactorNumberRange;
+ case FormControlType::InputTime:
+ case FormControlType::InputDatetimeLocal:
+ return kStepScaleFactorTime;
+ case FormControlType::InputMonth:
+ return kStepScaleFactorMonth;
+ case FormControlType::InputWeek:
+ return kStepScaleFactorWeek;
+ default:
+ MOZ_ASSERT(false, "Unrecognized input type");
+ return Decimal::nan();
+ }
+}
+
+Decimal HTMLInputElement::GetDefaultStep() const {
+ MOZ_ASSERT(DoesStepApply());
+
+ switch (mType) {
+ case FormControlType::InputDate:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ return kDefaultStep;
+ case FormControlType::InputTime:
+ case FormControlType::InputDatetimeLocal:
+ return kDefaultStepTime;
+ default:
+ MOZ_ASSERT(false, "Unrecognized input type");
+ return Decimal::nan();
+ }
+}
+
+void HTMLInputElement::SetUserInteracted(bool aInteracted) {
+ if (mUserInteracted == aInteracted) {
+ return;
+ }
+ mUserInteracted = aInteracted;
+ UpdateValidityElementStates(true);
+}
+
+void HTMLInputElement::UpdateInRange(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::INRANGE | ElementState::OUTOFRANGE);
+ if (!mHasRange || !IsCandidateForConstraintValidation()) {
+ return;
+ }
+ bool outOfRange = GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW) ||
+ GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW);
+ AddStatesSilently(outOfRange ? ElementState::OUTOFRANGE
+ : ElementState::INRANGE);
+}
+
+void HTMLInputElement::UpdateHasRange(bool aNotify) {
+ // There is a range if min/max applies for the type and if the element
+ // currently have a valid min or max.
+ const bool newHasRange = [&] {
+ if (!DoesMinMaxApply()) {
+ return false;
+ }
+ return !GetMinimum().isNaN() || !GetMaximum().isNaN();
+ }();
+
+ if (newHasRange == mHasRange) {
+ return;
+ }
+
+ mHasRange = newHasRange;
+ UpdateInRange(aNotify);
+}
+
+void HTMLInputElement::PickerClosed() { mPickerRunning = false; }
+
+JSObject* HTMLInputElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLInputElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+GetFilesHelper* HTMLInputElement::GetOrCreateGetFilesHelper(bool aRecursiveFlag,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mFileData);
+
+ if (aRecursiveFlag) {
+ if (!mFileData->mGetFilesRecursiveHelper) {
+ mFileData->mGetFilesRecursiveHelper = GetFilesHelper::Create(
+ GetFilesOrDirectoriesInternal(), aRecursiveFlag, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+
+ return mFileData->mGetFilesRecursiveHelper;
+ }
+
+ if (!mFileData->mGetFilesNonRecursiveHelper) {
+ mFileData->mGetFilesNonRecursiveHelper = GetFilesHelper::Create(
+ GetFilesOrDirectoriesInternal(), aRecursiveFlag, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+
+ return mFileData->mGetFilesNonRecursiveHelper;
+}
+
+void HTMLInputElement::UpdateEntries(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories) {
+ MOZ_ASSERT(mFileData && mFileData->mEntries.IsEmpty());
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ MOZ_ASSERT(global);
+
+ RefPtr<FileSystem> fs = FileSystem::Create(global);
+ if (NS_WARN_IF(!fs)) {
+ return;
+ }
+
+ Sequence<RefPtr<FileSystemEntry>> entries;
+ for (uint32_t i = 0; i < aFilesOrDirectories.Length(); ++i) {
+ RefPtr<FileSystemEntry> entry =
+ FileSystemEntry::Create(global, aFilesOrDirectories[i], fs);
+ MOZ_ASSERT(entry);
+
+ if (!entries.AppendElement(entry, fallible)) {
+ return;
+ }
+ }
+
+ // The root fileSystem is a DirectoryEntry object that contains only the
+ // dropped fileEntry and directoryEntry objects.
+ fs->CreateRoot(entries);
+
+ mFileData->mEntries = std::move(entries);
+}
+
+void HTMLInputElement::GetWebkitEntries(
+ nsTArray<RefPtr<FileSystemEntry>>& aSequence) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ Telemetry::Accumulate(Telemetry::BLINK_FILESYSTEM_USED, true);
+ aSequence.AppendElements(mFileData->mEntries);
+}
+
+already_AddRefed<nsINodeList> HTMLInputElement::GetLabels() {
+ if (!IsLabelable()) {
+ return nullptr;
+ }
+
+ return nsGenericHTMLElement::Labels();
+}
+
+void HTMLInputElement::MaybeFireInputPasswordRemoved() {
+ // We want this event to be fired only when the password field is removed
+ // from the DOM tree, not when it is released (ex, tab is closed). So don't
+ // fire an event when the password input field doesn't have a docshell.
+ Document* doc = GetComposedDoc();
+ nsIDocShell* container = doc ? doc->GetDocShell() : nullptr;
+ if (!container) {
+ return;
+ }
+
+ // Right now, only the password manager listens to the event and only listen
+ // to it under certain circumstances. So don't fire this event unless
+ // necessary.
+ if (!doc->ShouldNotifyFormOrPasswordRemoved()) {
+ return;
+ }
+
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *this, u"DOMInputPasswordRemoved"_ns, CanBubble::eNo,
+ ChromeOnlyDispatch::eYes);
+}
+
+} // namespace mozilla::dom
+
+#undef NS_ORIGINAL_CHECKED_VALUE