Source code

Revision control

Copy as Markdown

Other Tools

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