1
0
Fork 0
firefox/dom/html/HTMLInputElement.cpp
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

7536 lines
247 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- 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/CustomEvent.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/MouseEvent.h"
#include "mozilla/dom/MutationEventBinding.h"
#include "mozilla/dom/WheelEventBinding.h"
#include "mozilla/dom/WindowGlobalChild.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/Maybe.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_signon.h"
#include "mozilla/TextUtils.h"
#include "mozilla/Try.h"
#include "mozilla/Unused.h"
#include "nsAttrValueInlines.h"
#include "nsCRTGlue.h"
#include "nsIFilePicker.h"
#include "nsNetUtil.h"
#include "nsQueryObject.h"
#include "HTMLDataListElement.h"
#include "HTMLFormSubmissionConstants.h"
#include "mozilla/glean/DomMetrics.h"
#include "nsBaseCommandController.h"
#include "nsIStringBundle.h"
#include "nsFocusManager.h"
#include "nsColorControlFrame.h"
#include "nsFileControlFrame.h"
#include "nsNumberControlFrame.h"
#include "nsSearchControlFrame.h"
#include "nsPIDOMWindow.h"
#include "nsRepeatService.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 "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 constexpr nsAttrValue::EnumTableEntry 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},
};
// Default type is 'text'.
static constexpr const nsAttrValue::EnumTableEntry* kInputDefaultType =
&kInputTypeTable[std::size(kInputTypeTable) - 1];
static constexpr nsAttrValue::EnumTableEntry kCaptureTable[] = {
{"user", nsIFilePicker::captureUser},
{"environment", nsIFilePicker::captureEnv},
{"", nsIFilePicker::captureDefault},
};
static constexpr const nsAttrValue::EnumTableEntry* 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()) {
nsresult rv = NS_NewLocalFile(prefStr, getter_AddRefs(localFile));
Unused << NS_WARN_IF(NS_FAILED(rv));
}
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, 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);
if (!tmp) {
return NS_OK;
}
// 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)) {
#ifdef MOZ_WIDGET_ANDROID
// Android 13 or later cannot enumerate files into user directory due to
// no permission. So we store file list into file picker.
FallibleTArray<RefPtr<BlobImpl>> filesInWebKitDirectory;
nsCOMPtr<nsISimpleEnumerator> iter;
if (NS_SUCCEEDED(
mFilePicker->GetDomFilesInWebKitDirectory(getter_AddRefs(iter))) &&
iter) {
nsCOMPtr<nsISupports> supports;
bool loop = true;
while (NS_SUCCEEDED(iter->HasMoreElements(&loop)) && loop) {
iter->GetNext(getter_AddRefs(supports));
if (supports) {
RefPtr<BlobImpl> file = static_cast<File*>(supports.get())->Impl();
MOZ_ASSERT(file);
if (!filesInWebKitDirectory.AppendElement(file, fallible)) {
return nsresult::NS_ERROR_OUT_OF_MEMORY;
}
}
}
}
if (!filesInWebKitDirectory.IsEmpty()) {
dispatchChangeEventCallback->Callback(NS_OK, filesInWebKitDirectory);
return NS_OK;
}
#endif
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 IsPickerBlocked(Document* aDoc) {
if (aDoc->ConsumeTransientUserGestureActivation()) {
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();
RefPtr<BrowsingContext> bc = doc->GetBrowsingContext();
if (!bc) {
return NS_ERROR_FAILURE;
}
if (IsPickerBlocked(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(bc, 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;
SetStates(ElementState::OPEN, 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();
RefPtr<BrowsingContext> bc = doc->GetBrowsingContext();
if (!bc) {
return NS_ERROR_FAILURE;
}
if (IsPickerBlocked(doc)) {
return NS_OK;
}
// Get Loc title
nsAutoString title;
nsAutoString okButtonLabel;
if (aType == FILE_PICKER_DIRECTORY) {
nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"DirectoryUpload", doc, title);
nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"DirectoryPickerOkButtonLabel", doc,
okButtonLabel);
} else {
nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"FileUpload", doc, 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(bc, title, mode);
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;
SetStates(ElementState::OPEN, true);
}
return rv;
}
HTMLInputElement::gUploadLastDir->FetchDirectoryAndDisplayPicker(
doc, filePicker, callback);
mPickerRunning = true;
SetStates(ElementState::OPEN, 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),
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, /* aNotify */ false,
/* aSetValueChanged */ 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) {
glean::dom::webkit_directory_used
.EnumGet(glean::dom::WebkitDirectoryUsedLabel::eTrue)
.Add();
}
}
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) {
ResetDirFormAssociatedElement(this, aNotify, HasDirAuto());
}
// 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, /* aSetValueChanged */ 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)) {
ResetDirFormAssociatedElement(this, aNotify, true);
} 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;
}
Decimal HTMLInputElement::GetValueIfStepped(int32_t aStep,
StepCallerType aCallerType,
ErrorResult& aRv) {
constexpr auto kNaN = Decimal::nan();
if (!DoStepDownStepUpApply()) {
aRv.ThrowInvalidStateError("Step doesn't apply to this input type");
return kNaN;
}
Decimal stepBase = GetStepBase();
Decimal step = GetStep();
if (step == kStepAny) {
if (aCallerType != StepCallerType::ForUserEvent) {
aRv.ThrowInvalidStateError("Can't step an input with step=\"any\"");
return kNaN;
}
// 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 kNaN;
}
}
}
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 kNaN;
}
return value;
}
void HTMLInputElement::ApplyStep(int32_t aStep, ErrorResult& aRv) {
Decimal nextStep = GetValueIfStepped(aStep, StepCallerType::ForScript, aRv);
if (aRv.Failed() || !nextStep.isFinite()) {
return;
}
// 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);
}
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://
Unused << NS_GetFileFromURLSpec(NS_ConvertUTF16toUTF8(aFileNames[i]),
getter_AddRefs(file));
}
if (!file) {
// this is no "file://", try as local file
Unused << NS_NewLocalFile(aFileNames[i], 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, 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::SetDateTimePickerState(bool aIsOpen) {
SetStates(ElementState::OPEN, aIsOpen);
}
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 !!GetExtantTextEditor(); }
TextEditor* HTMLInputElement::GetTextEditorFromState() {
TextControlState* state = GetEditorState();
if (state) {
return state->GetTextEditor();
}
return nullptr;
}
TextEditor* HTMLInputElement::GetTextEditor() {
return GetTextEditorFromState();
}
TextEditor* HTMLInputElement::GetExtantTextEditor() const {
const TextControlState* const state = GetEditorState();
if (!state) {
return nullptr;
}
return state->GetExtantTextEditor();
}
nsISelectionController* HTMLInputElement::GetSelectionController() {
TextControlState* state = GetEditorState();
if (state) {
return state->GetSelectionController();
}
return nullptr;
}
nsFrameSelection* HTMLInputElement::GetIndependentFrameSelection() const {
TextControlState* state = GetEditorState();
if (state) {
return state->GetIndependentFrameSelection();
}
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.
if (nsFileControlFrame* f = do_QueryFrame(GetPrimaryFrame())) {
f->SelectedFilesUpdated();
}
// 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, /* aNotify */ true, /* aSetValueChanged */ true);
}
void HTMLInputElement::DoSetChecked(bool aChecked, bool aNotify,
bool aSetValueChanged,
bool aUpdateOtherElement) {
// 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, aUpdateOtherElement);
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, bool aUpdateOtherElement) {
if (aUpdateOtherElement) {
// Its possible for multiple radio input to have their checkedness set to
// true, so we need to deselect all of them.
VisitGroup([self = RefPtr{this}](HTMLInputElement* aRadio) {
if (aRadio != self) {
aRadio->SetCheckedInternal(false, true);
}
return 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()) {
WidgetPointerEvent event(true, ePointerClick, nullptr);
event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_KEYBOARD;
// pointerId definition in Pointer Events:
// > The pointerId value of -1 MUST be reserved and used to indicate events
// > that were generated by something other than a pointing device.
event.pointerId = -1;
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->GetIndependentFrameSelection();
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() {
// FIXME(emilio): Should we try to call Select(), which will avoid flushing?
if (nsTextControlFrame* tf =
do_QueryFrame(GetPrimaryFrame(FlushType::Frames))) {
tf->SelectAll();
}
}
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 {
// 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;
}
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)) {
if (nsTextControlFrame* tcf = do_QueryFrame(GetPrimaryFrame())) {
tcf->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)) {
// We handle focus here.
// FIXME(emilio): Why is this needed? If it is it should be moved to
// nsRangeFrame::ElementStateChanged.
if (nsIFrame* frame = GetPrimaryFrame()) {
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);
}
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, /* aNotify */ true,
/* aSetValueChanged */ 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(/* aValue */ true, /* aNotify */ true,
/* aSetValueChanged */ 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.
// XXXedgar: We exclude the radio type because `mItemData` is already used to
// store the originally selected radio button above.
if (mForm && mType != FormControlType::InputRadio) {
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();
if ((mType == FormControlType::InputSubmit ||
mType == FormControlType::InputImage) &&
aVisitor.mDOMEvent) {
if (auto* mouseEvent = aVisitor.mDOMEvent->AsMouseEvent()) {
const CSSIntPoint pt = RoundedToInt(mouseEvent->OffsetPoint());
if (auto* imageClickedPoint = static_cast<CSSIntPoint*>(
GetProperty(nsGkAtoms::imageClickedPoint))) {
// Ensures that a dispatched event's clicked point is not the default
// value.
*imageClickedPoint = pt;
}
}
}
}
}
void HTMLInputElement::MaybeDispatchWillBlur(EventChainVisitor& aVisitor) {
if (!CreatesDateTimeWidget() || !aVisitor.mEvent->IsTrusted()) {
return;
}
RefPtr<Element> dateTimeBoxElement = GetDateTimeBoxElement();
if (!dateTimeBoxElement) {
return;
}
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(GetOwnerGlobal()))) {
return;
}
if (!aVisitor.mDOMEvent) {
RefPtr<Event> event = EventDispatcher::CreateEvent(
aVisitor.mEvent->mOriginalTarget, aVisitor.mPresContext,
aVisitor.mEvent, u""_ns);
event.swap(aVisitor.mDOMEvent);
}
JS::Rooted<JS::Value> detail(jsapi.cx(), JS::NullHandleValue);
if (NS_WARN_IF(!ToJSValue(jsapi.cx(), aVisitor.mDOMEvent, &detail))) {
return;
}
// Event is dispatched to closed-shadow tree and doesn't bubble.
RefPtr<CustomEvent> event =
NS_NewDOMCustomEvent(OwnerDoc(), aVisitor.mPresContext, nullptr);
event->InitCustomEvent(jsapi.cx(), u"MozDateTimeWillBlur"_ns,
/* CanBubble */ false,
/* Cancelable */ false, detail);
event->SetTrusted(true);
dateTimeBoxElement->DispatchEvent(*event);
}
nsresult HTMLInputElement::PreHandleEvent(EventChainVisitor& aVisitor) {
if (aVisitor.mItemFlags & NS_PRE_HANDLE_BLUR_EVENT) {
MOZ_ASSERT(aVisitor.mEvent->mMessage == eBlur);
// TODO(emilio): This should probably happen only if the event is trusted?
FireChangeEventIfNeeded();
MaybeDispatchWillBlur(aVisitor);
}
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 = GetValueIfStepped(aDirection, StepCallerType::ForUserEvent,
IgnoreErrors());
if (!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 check and consume user activation.
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) {
SelectAll();
}
}
}
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()) {
// Radio button navigation needs to check visibility, so flush
// to ensure visibility is up to date.
if (Document* doc = GetComposedDoc()) {
doc->FlushPendingNotifications(
FlushType::EnsurePresShellInitAndFrames);
}
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;
}
case eWheel: {
if (StaticPrefs::
dom_input_number_and_range_modified_by_mousewheel()) {
// 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 (nsFocusManager::GetFocusedElementStatic() == this) {
StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1
: 1);
FireChangeEventIfNeeded();
aVisitor.mEvent->PreventDefault();
}
} else if (mType == FormControlType::InputRange &&
nsFocusManager::GetFocusedElementStatic() == 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;
}
case ePointerClick: {
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->GetButton();
if (clearButton &&
aVisitor.mEvent->mOriginalTarget == clearButton) {
SetUserInput(EmptyString(),
*nsContentUtils::GetSystemPrincipal());
}
}
} else if (mType == FormControlType::InputPassword) {
if (nsTextControlFrame* textControlFrame =
do_QueryFrame(GetPrimaryFrame())) {
auto* reveal = textControlFrame->GetButton();
if (reveal && aVisitor.mEvent->mOriginalTarget == reveal) {
SetRevealPassword(!RevealPassword());
}
}
}
}
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) {
if (aVisitor.mItemFlags & NS_IN_SUBMIT_CLICK) {
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;
}
// https://html.spec.whatwg.org/#checkbox-state-(type=checkbox):input-activation-behavior
// If element is connected, fire input and change event
if (mCheckedIsToggled && IsInComposedDoc()) {
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(/* aValue */ false, /* aNotify */ true,
/* aSetValueChanged */ true);
}
} else if (oldType == FormControlType::InputCheckbox) {
bool originalIndeterminateValue =
!!(aVisitor.mItemFlags & NS_ORIGINAL_INDETERMINATE_VALUE);
SetIndeterminateInternal(originalIndeterminateValue, false);
DoSetChecked(originalCheckedValue, /* aNotify */ true,
/* aSetValueChanged */ 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
ResetDirFormAssociatedElement(this, false, HasDirAuto());
// 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;
EventTarget* 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;
if (aForm) {
target = aForm;
aForm->mHasPendingPasswordEvent = true;
} else {
target = this;
}
} else if (mType == FormControlType::InputEmail ||
mType == FormControlType::InputText) {
// Don't fire a username event if:
// - we have a pending event
// - username only forms are not supported
// fire event if we have a username field without a form with the
// autcomplete value of username
if (!StaticPrefs::signon_usernameOnlyForm_enabled()) {
return;
}
if (aForm) {
if (aForm->mHasPendingPossibleUsernameEvent) {
return;
}
aForm->mHasPendingPossibleUsernameEvent = true;
target = aForm;
} else {
nsAutoString autocompleteValue;
GetAutocomplete(autocompleteValue);
if (!autocompleteValue.EqualsASCII("username")) {
return;
}
target = GetComposedDoc();
}
eventType = u"DOMPossibleUsernameInputAdded"_ns;
} else {
return;
}
RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher(
target, eventType, CanBubble::eYes, ChromeOnlyDispatch::eYes);
dispatcher->PostDOMEvent();
}
void HTMLInputElement::UnbindFromTree(UnbindContext& aContext) {
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();
nsGenericHTMLFormControlElementWithState::UnbindFromTree(aContext);
// 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 might change auto directionality of this or the assigned slot
const bool autoDirAssociated = IsAutoDirectionalityAssociated(mType);
if (IsAutoDirectionalityAssociated(oldType) != autoDirAssociated) {
ResetDirFormAssociatedElement(this, aNotify, true);
}
// Special case for <input type=tel> as specified in
// https://html.spec.whatwg.org/multipage/dom.html#the-directionality
if (!HasDirAuto() && (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.
aValue.AssignASCII(result.mResult.toString().c_str());
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) {
aValue.AssignASCII(value.toString().c_str());
}
} 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) {
static_assert(
FormControlType(kInputDefaultType->value) == FormControlType::InputText,
"Someone forgot to update kInputDefaultType when adding a new "
"input type.");
static_assert(
FormControlType(kInputTypeTable[std::size(kInputTypeTable) - 1].value) ==
FormControlType::InputText,
"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.
MOZ_ASSERT(&Span(kInputTypeTable).Last<1>()[0] == kInputDefaultType);
aResult.ParseEnumValue(aValue, Span(kInputTypeTable).Last<1>(), 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 GetExtantControllers();
}
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 = 0, selStart = 0;
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 = 0, selStart = 0;
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. Consume user activation given element's relevant global object.
// InitFilePicker() and InitColorPicker() consume it themselves,
// so only consume in this function if not those.
// Step 4. 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 5. 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;
}
// See Step 3.
OwnerDoc()->ConsumeTransientUserGestureActivation();
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 = 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);
}
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(), /* aNotify */ true,
/* aSetValueChanged */ 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(), /* aNotify */ false,
/* aSetValueChanged */ false, mForm || IsInComposedDoc());
}
// 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(), 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, /* aNotify */ true, /* aSetValueChanged */ 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, mForm || IsInComposedDoc());
} 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(IsFocusableFlags aFlags,
bool* aIsFocusable, int32_t* aTabIndex) {
if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable(
aFlags, aIsFocusable, aTabIndex)) {
return true;
}
if (IsDisabled()) {
*aIsFocusable = false;
return true;
}
if (IsSingleLineTextControl(false) || mType == FormControlType::InputRange) {
*aIsFocusable = true;
return false;
}
const bool defaultFocusable = IsFormControlDefaultFocusable(aFlags);
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.
// Make it tabbable if nothing in group is selected and it is the first radio
// button.
auto* container = GetCurrentRadioGroupContainer();
if (!container) {
*aIsFocusable = defaultFocusable;
return false;
}
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
// If there is a selected radio button but it is disabled or hidden, it
// shouldn't be considered as selected for this check. Otherwise, the entire
// group will be unreachable with the tab key.
HTMLInputElement* selectedRadio = container->GetCurrentRadioButton(name);
if ((selectedRadio && !selectedRadio->Disabled() &&
selectedRadio->GetPrimaryFrame()) ||
container->GetFirstRadioButton(name) != this) {
*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;
}
void HTMLInputElement::VisitGroup(
const RadioGroupContainer::VisitCallback& aCallback) {
if (auto* container = GetCurrentRadioGroupContainer()) {
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
container->WalkRadioGroup(name, aCallback);
return;
}
aCallback(this);
}
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() &&
!(DoesReadWriteApply() && 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;
}
Maybe<int32_t> HTMLInputElement::GetNumberInputCols() const {
// This logic is ported from WebKit, see
// https://github.com/whatwg/html/issues/10390
struct RenderSize {
uint32_t mBeforeDecimal = 0;
uint32_t mAfterDecimal = 0;
RenderSize Max(const RenderSize& aOther) const {
return {std::max(mBeforeDecimal, aOther.mBeforeDecimal),
std::max(mAfterDecimal, aOther.mAfterDecimal)};
}
static RenderSize From(const Decimal& aValue) {
MOZ_ASSERT(aValue.isFinite());
nsAutoCString tmp;
tmp.AppendInt(aValue.value().coefficient());
const uint32_t sizeOfDigits = tmp.Length();
const uint32_t sizeOfSign = aValue.isNegative() ? 1 : 0;
const int32_t exponent = aValue.exponent();
if (exponent >= 0) {
return {sizeOfSign + sizeOfDigits, 0};
}
const int32_t sizeBeforeDecimalPoint = exponent + int32_t(sizeOfDigits);
if (sizeBeforeDecimalPoint > 0) {
// In case of "123.456"
return {sizeOfSign + sizeBeforeDecimalPoint,
sizeOfDigits - sizeBeforeDecimalPoint};
}
// In case of "0.00012345"
const uint32_t sizeOfZero = 1;
const uint32_t numberOfZeroAfterDecimalPoint = -sizeBeforeDecimalPoint;
return {sizeOfSign + sizeOfZero,
numberOfZeroAfterDecimalPoint + sizeOfDigits};
}
};
if (mType != FormControlType::InputNumber) {
return {};
}
Decimal min = GetMinimum();
if (!min.isFinite()) {
return {};
}
Decimal max = GetMaximum();
if (!max.isFinite()) {
return {};
}
Decimal step = GetStep();
if (step == kStepAny) {
return {};
}
MOZ_ASSERT(step.isFinite());
RenderSize size = RenderSize::From(min).Max(
RenderSize::From(max).Max(RenderSize::From(step)));
return Some(size.mBeforeDecimal + size.mAfterDecimal +
(size.mAfterDecimal ? 1 : 0));
}
Maybe<int32_t> HTMLInputElement::GetCols() {
if (const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::size);
attr && attr->Type() == nsAttrValue::eInteger) {
int32_t cols = attr->GetIntegerValue();
if (cols > 0) {
return Some(cols);
}
}
if (Maybe<int32_t> cols = GetNumberInputCols(); cols && *cols > 0) {
return cols;
}
return {};
}
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 (mLastValueChangeWasInteractive &&
State().HasState(ElementState::AUTOFILL)) {
RemoveStates(ElementState::AUTOFILL | ElementState::AUTOFILL_PREVIEW);
}
}
if (aNewValueEmpty != IsValueEmpty()) {
SetStates(ElementState::VALUE_EMPTY, aNewValueEmpty);
UpdatePlaceholderShownState();
}
UpdateAllValidityStates(true);
ResetDirFormAssociatedElement(this, true, HasDirAuto(), 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;
SetStates(ElementState::OPEN, 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;
}
glean::dom::blink_filesystem_used
.EnumGet(glean::dom::BlinkFilesystemUsedLabel::eTrue)
.Add();
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