diff options
Diffstat (limited to 'layout/forms/nsFileControlFrame.cpp')
-rw-r--r-- | layout/forms/nsFileControlFrame.cpp | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/layout/forms/nsFileControlFrame.cpp b/layout/forms/nsFileControlFrame.cpp new file mode 100644 index 000000000..659371615 --- /dev/null +++ b/layout/forms/nsFileControlFrame.cpp @@ -0,0 +1,506 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsFileControlFrame.h" + +#include "nsGkAtoms.h" +#include "nsCOMPtr.h" +#include "nsIDocument.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/HTMLButtonElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/Preferences.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "mozilla/EventStates.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/FileList.h" +#include "nsIDOMDragEvent.h" +#include "nsIDOMFileList.h" +#include "nsContentList.h" +#include "nsIDOMMutationEvent.h" +#include "nsTextNode.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* +NS_NewFileControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsFileControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame) + +nsFileControlFrame::nsFileControlFrame(nsStyleContext* aContext) + : nsBlockFrame(aContext) +{ + AddStateBits(NS_BLOCK_FLOAT_MGR); +} + + +void +nsFileControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) +{ + nsBlockFrame::Init(aContent, aParent, aPrevInFlow); + + mMouseListener = new DnDListener(this); +} + +void +nsFileControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + ENSURE_TRUE(mContent); + + // Remove the events. + if (mContent) { + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("drop"), + mMouseListener, false); + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("dragover"), + mMouseListener, false); + } + + nsContentUtils::DestroyAnonymousContent(&mTextContent); + nsContentUtils::DestroyAnonymousContent(&mBrowseFilesOrDirs); + + mMouseListener->ForgetFrame(); + nsBlockFrame::DestroyFrom(aDestructRoot); +} + +static already_AddRefed<Element> +MakeAnonButton(nsIDocument* aDoc, const char* labelKey, + HTMLInputElement* aInputElement, + const nsAString& aAccessKey) +{ + RefPtr<Element> button = aDoc->CreateHTMLElement(nsGkAtoms::button); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + button->SetIsNativeAnonymousRoot(); + button->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("button"), false); + + // Set the file picking button text depending on the current locale. + nsXPIDLString buttonTxt; + nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + labelKey, buttonTxt); + + // Set the browse button text. It's a bit of a pain to do because we want to + // make sure we are not notifying. + RefPtr<nsTextNode> textContent = + new nsTextNode(button->NodeInfo()->NodeInfoManager()); + + textContent->SetText(buttonTxt, false); + + nsresult rv = button->AppendChildTo(textContent, false); + if (NS_FAILED(rv)) { + return nullptr; + } + + // Make sure access key and tab order for the element actually redirect to the + // file picking button. + RefPtr<HTMLButtonElement> buttonElement = + HTMLButtonElement::FromContentOrNull(button); + + if (!aAccessKey.IsEmpty()) { + buttonElement->SetAccessKey(aAccessKey); + } + + // Both elements are given the same tab index so that the user can tab + // to the file control at the correct index, and then between the two + // buttons. + int32_t tabIndex; + aInputElement->GetTabIndex(&tabIndex); + buttonElement->SetTabIndex(tabIndex); + + return button.forget(); +} + +nsresult +nsFileControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc(); + + RefPtr<HTMLInputElement> fileContent = HTMLInputElement::FromContentOrNull(mContent); + + // The access key is transferred to the "Choose files..." button only. In + // effect that access key allows access to the control via that button, then + // the user can tab between the two buttons. + nsAutoString accessKey; + fileContent->GetAccessKey(accessKey); + + mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent, accessKey); + if (!mBrowseFilesOrDirs || !aElements.AppendElement(mBrowseFilesOrDirs)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Create and setup the text showing the selected files. + RefPtr<NodeInfo> nodeInfo; + nodeInfo = doc->NodeInfoManager()->GetNodeInfo(nsGkAtoms::label, nullptr, + kNameSpaceID_XUL, + nsIDOMNode::ELEMENT_NODE); + NS_TrustedNewXULElement(getter_AddRefs(mTextContent), nodeInfo.forget()); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + mTextContent->SetIsNativeAnonymousRoot(); + mTextContent->SetAttr(kNameSpaceID_None, nsGkAtoms::crop, + NS_LITERAL_STRING("center"), false); + + // Update the displayed text to reflect the current element's value. + nsAutoString value; + HTMLInputElement::FromContent(mContent)->GetDisplayFileName(value); + UpdateDisplayedValue(value, false); + + if (!aElements.AppendElement(mTextContent)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // We should be able to interact with the element by doing drag and drop. + mContent->AddSystemEventListener(NS_LITERAL_STRING("drop"), + mMouseListener, false); + mContent->AddSystemEventListener(NS_LITERAL_STRING("dragover"), + mMouseListener, false); + + SyncDisabledState(); + + return NS_OK; +} + +void +nsFileControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mBrowseFilesOrDirs) { + aElements.AppendElement(mBrowseFilesOrDirs); + } + + if (mTextContent) { + aElements.AppendElement(mTextContent); + } +} + +NS_QUERYFRAME_HEAD(nsFileControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +void +nsFileControlFrame::SetFocus(bool aOn, bool aRepaint) +{ +} + +static void +AppendBlobImplAsDirectory(nsTArray<OwningFileOrDirectory>& aArray, + BlobImpl* aBlobImpl, + nsIContent* aContent) +{ + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aBlobImpl->IsDirectory()); + + nsAutoString fullpath; + ErrorResult err; + aBlobImpl->GetMozFullPath(fullpath, err); + if (err.Failed()) { + err.SuppressException(); + return; + } + + nsCOMPtr<nsIFile> file; + NS_ConvertUTF16toUTF8 path(fullpath); + nsresult rv = NS_NewNativeLocalFile(path, true, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsPIDOMWindowInner* inner = aContent->OwnerDoc()->GetInnerWindow(); + if (!inner || !inner->IsCurrentInnerWindow()) { + return; + } + + RefPtr<Directory> directory = + Directory::Create(inner, file); + MOZ_ASSERT(directory); + + OwningFileOrDirectory* element = aArray.AppendElement(); + element->SetAsDirectory() = directory; +} + +/** + * This is called when we receive a drop or a dragover. + */ +NS_IMETHODIMP +nsFileControlFrame::DnDListener::HandleEvent(nsIDOMEvent* aEvent) +{ + NS_ASSERTION(mFrame, "We should have been unregistered"); + + bool defaultPrevented = false; + aEvent->GetDefaultPrevented(&defaultPrevented); + if (defaultPrevented) { + return NS_OK; + } + + nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent); + if (!dragEvent) { + return NS_OK; + } + + nsCOMPtr<nsIDOMDataTransfer> dataTransfer; + dragEvent->GetDataTransfer(getter_AddRefs(dataTransfer)); + if (!IsValidDropData(dataTransfer)) { + return NS_OK; + } + + + nsCOMPtr<nsIContent> content = mFrame->GetContent(); + bool supportsMultiple = content && content->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple); + if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) { + dataTransfer->SetDropEffect(NS_LITERAL_STRING("none")); + aEvent->StopPropagation(); + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("dragover")) { + // Prevent default if we can accept this drag data + aEvent->PreventDefault(); + return NS_OK; + } + + if (eventType.EqualsLiteral("drop")) { + aEvent->StopPropagation(); + aEvent->PreventDefault(); + + NS_ASSERTION(content, "The frame has no content???"); + + HTMLInputElement* inputElement = HTMLInputElement::FromContent(content); + NS_ASSERTION(inputElement, "No input element for this file upload control frame!"); + + nsCOMPtr<nsIDOMFileList> fileList; + dataTransfer->GetFiles(getter_AddRefs(fileList)); + + RefPtr<BlobImpl> webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsTArray<OwningFileOrDirectory> array; + if (webkitDir) { + AppendBlobImplAsDirectory(array, webkitDir, content); + inputElement->MozSetDndFilesAndDirectories(array); + } else { + bool blinkFileSystemEnabled = + Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false); + if (blinkFileSystemEnabled) { + FileList* files = static_cast<FileList*>(fileList.get()); + if (files) { + for (uint32_t i = 0; i < files->Length(); ++i) { + File* file = files->Item(i); + if (file) { + if (file->Impl() && file->Impl()->IsDirectory()) { + AppendBlobImplAsDirectory(array, file->Impl(), content); + } else { + OwningFileOrDirectory* element = array.AppendElement(); + element->SetAsFile() = file; + } + } + } + } + } + + // This is rather ugly. Pass the directories as Files using SetFiles, + // but then if blink filesystem API is enabled, it wants + // FileOrDirectory array. + inputElement->SetFiles(fileList, true); + if (blinkFileSystemEnabled) { + inputElement->UpdateEntries(array); + } + nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content, + NS_LITERAL_STRING("input"), true, + false); + nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content, + NS_LITERAL_STRING("change"), true, + false); + } + } + + return NS_OK; +} + +nsresult +nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory(nsIDOMFileList* aFileList, + BlobImpl** aBlobImpl) +{ + *aBlobImpl = nullptr; + + HTMLInputElement* inputElement = + HTMLInputElement::FromContent(mFrame->GetContent()); + bool webkitDirPicker = + Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + inputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory); + if (!webkitDirPicker) { + return NS_OK; + } + + if (!aFileList) { + return NS_ERROR_FAILURE; + } + + FileList* files = static_cast<FileList*>(aFileList); + // webkitdirectory doesn't care about the length of the file list but + // only about the first item on it. + uint32_t len = files->Length(); + if (len) { + File* file = files->Item(0); + if (file) { + BlobImpl* impl = file->Impl(); + if (impl && impl->IsDirectory()) { + RefPtr<BlobImpl> retVal = impl; + retVal.swap(*aBlobImpl); + return NS_OK; + } + } + } + + return NS_ERROR_FAILURE; +} + +bool +nsFileControlFrame::DnDListener::IsValidDropData(nsIDOMDataTransfer* aDOMDataTransfer) +{ + nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(aDOMDataTransfer); + NS_ENSURE_TRUE(dataTransfer, false); + + // We only support dropping files onto a file upload control + nsTArray<nsString> types; + dataTransfer->GetTypes(types, *nsContentUtils::GetSystemPrincipal()); + + return types.Contains(NS_LITERAL_STRING("Files")); +} + +bool +nsFileControlFrame::DnDListener::CanDropTheseFiles(nsIDOMDataTransfer* aDOMDataTransfer, + bool aSupportsMultiple) +{ + nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(aDOMDataTransfer); + NS_ENSURE_TRUE(dataTransfer, false); + + nsCOMPtr<nsIDOMFileList> fileList; + dataTransfer->GetFiles(getter_AddRefs(fileList)); + + RefPtr<BlobImpl> webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + // Just check if either there isn't webkitdirectory attribute, or + // fileList has a directory which can be dropped to the element. + // No need to use webkitDir for anything here. + NS_ENSURE_SUCCESS(rv, false); + + uint32_t listLength = 0; + if (fileList) { + fileList->GetLength(&listLength); + } + return listLength <= 1 || aSupportsMultiple; +} + +nscoord +nsFileControlFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + nscoord result; + DISPLAY_MIN_WIDTH(this, result); + + // Our min width is our pref width + result = GetPrefISize(aRenderingContext); + return result; +} + +void +nsFileControlFrame::SyncDisabledState() +{ + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, + EmptyString(), true); + } else { + mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + } +} + +nsresult +nsFileControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::tabindex) { + if (aModType == nsIDOMMutationEvent::REMOVAL) { + mBrowseFilesOrDirs->UnsetAttr(aNameSpaceID, aAttribute, true); + } else { + nsAutoString value; + mContent->GetAttr(aNameSpaceID, aAttribute, value); + mBrowseFilesOrDirs->SetAttr(aNameSpaceID, aAttribute, value, true); + } + } + + return nsBlockFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +void +nsFileControlFrame::ContentStatesChanged(EventStates aStates) +{ + if (aStates.HasState(NS_EVENT_STATE_DISABLED)) { + nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this)); + } +} + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsFileControlFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("FileControl"), aResult); +} +#endif + +void +nsFileControlFrame::UpdateDisplayedValue(const nsAString& aValue, bool aNotify) +{ + mTextContent->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, aNotify); +} + +nsresult +nsFileControlFrame::SetFormProperty(nsIAtom* aName, + const nsAString& aValue) +{ + if (nsGkAtoms::value == aName) { + UpdateDisplayedValue(aValue, true); + } + return NS_OK; +} + +void +nsFileControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + BuildDisplayListForInline(aBuilder, aDirtyRect, aLists); +} + +#ifdef ACCESSIBILITY +a11y::AccType +nsFileControlFrame::AccessibleType() +{ + return a11y::eHTMLFileInputType; +} +#endif + +//////////////////////////////////////////////////////////// +// Mouse listener implementation + +NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener, + nsIDOMEventListener) |