diff options
Diffstat (limited to 'layout/forms/nsFileControlFrame.cpp')
-rw-r--r-- | layout/forms/nsFileControlFrame.cpp | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/layout/forms/nsFileControlFrame.cpp b/layout/forms/nsFileControlFrame.cpp new file mode 100644 index 0000000000..cc7d56a913 --- /dev/null +++ b/layout/forms/nsFileControlFrame.cpp @@ -0,0 +1,578 @@ +/* -*- 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 "nsFileControlFrame.h" + +#include "nsGkAtoms.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/DragEvent.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/FileList.h" +#include "mozilla/dom/HTMLButtonElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/intl/Segmenter.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/TextEditor.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsIFile.h" +#include "nsLayoutUtils.h" +#include "nsTextNode.h" +#include "nsTextFrame.h" +#include "gfxContext.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewFileControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) + nsFileControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame) + +nsFileControlFrame::nsFileControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBlockFrame(aStyle, aPresContext, kClassID) { + AddStateBits(NS_BLOCK_FLOAT_MGR); +} + +void nsFileControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBlockFrame::Init(aContent, aParent, aPrevInFlow); + + mMouseListener = new DnDListener(this); +} + +bool nsFileControlFrame::CropTextToWidth(gfxContext& aRenderingContext, + const nsIFrame* aFrame, nscoord aWidth, + nsString& aText) { + if (aText.IsEmpty()) { + return false; + } + + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetFontMetricsForFrame(aFrame, 1.0f); + + // see if the text will completely fit in the width given + if (const nscoord textWidth = nsLayoutUtils::AppUnitWidthOfStringBidi( + aText, aFrame, *fm, aRenderingContext); + textWidth <= aWidth) { + return false; + } + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis(); + + // see if the width is even smaller than the ellipsis + fm->SetTextRunRTL(false); + const nscoord ellipsisWidth = + nsLayoutUtils::AppUnitWidthOfString(kEllipsis, *fm, drawTarget); + if (ellipsisWidth >= aWidth) { + aText = kEllipsis; + return true; + } + + // determine how much of the string will fit in the max width + nscoord totalWidth = ellipsisWidth; + const Span text(aText); + intl::GraphemeClusterBreakIteratorUtf16 leftIter(text); + intl::GraphemeClusterBreakReverseIteratorUtf16 rightIter(text); + uint32_t leftPos = 0; + uint32_t rightPos = aText.Length(); + nsAutoString leftString, rightString; + + while (leftPos < rightPos) { + Maybe<uint32_t> pos = leftIter.Next(); + Span chars = text.FromTo(leftPos, *pos); + nscoord charWidth = + nsLayoutUtils::AppUnitWidthOfString(chars, *fm, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + leftString.Append(chars); + leftPos = *pos; + totalWidth += charWidth; + + if (leftPos >= rightPos) { + break; + } + + pos = rightIter.Next(); + chars = text.FromTo(*pos, rightPos); + charWidth = nsLayoutUtils::AppUnitWidthOfString(chars, *fm, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + rightString.Insert(chars, 0); + rightPos = *pos; + totalWidth += charWidth; + } + + aText = leftString + kEllipsis + rightString; + return true; +} + +void nsFileControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aMetrics, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + // Restore the uncropped filename. + nsAutoString filename; + HTMLInputElement::FromNode(mContent)->GetDisplayFileName(filename); + + bool done = false; + while (true) { + UpdateDisplayedValue(filename, false); // update the text node + AddStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION); + LinesBegin()->MarkDirty(); + nsBlockFrame::Reflow(aPresContext, aMetrics, aReflowInput, aStatus); + if (done) { + break; + } + nscoord lineISize = LinesBegin()->ISize(); + const auto cbWM = aMetrics.GetWritingMode(); + const auto wm = GetWritingMode(); + nscoord iSize = + wm.IsOrthogonalTo(cbWM) ? aMetrics.BSize(cbWM) : aMetrics.ISize(cbWM); + auto bp = GetLogicalUsedBorderAndPadding(wm); + nscoord contentISize = iSize - bp.IStartEnd(wm); + if (lineISize > contentISize) { + // The filename overflows - crop it and reflow again (once). + // NOTE: the label frame might have bidi-continuations + auto* labelFrame = mTextContent->GetPrimaryFrame(); + nscoord labelBP = + labelFrame->GetLogicalUsedBorderAndPadding(wm).IStartEnd(wm); + auto* lastLabelCont = labelFrame->LastContinuation(); + if (lastLabelCont != labelFrame) { + labelBP += + lastLabelCont->GetLogicalUsedBorderAndPadding(wm).IStartEnd(wm); + } + nscoord availableISizeForLabel = contentISize; + if (auto* buttonFrame = mBrowseFilesOrDirs->GetPrimaryFrame()) { + availableISizeForLabel -= + buttonFrame->ISize(wm) + + buttonFrame->GetLogicalUsedMargin(wm).IStartEnd(wm); + } + if (CropTextToWidth(*aReflowInput.mRenderingContext, labelFrame, + availableISizeForLabel - labelBP, filename)) { + nsBlockFrame::DidReflow(aPresContext, &aReflowInput); + aStatus.Reset(); + labelFrame->MarkSubtreeDirty(); + labelFrame->AddStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION); + mCachedMinISize = NS_INTRINSIC_ISIZE_UNKNOWN; + mCachedPrefISize = NS_INTRINSIC_ISIZE_UNKNOWN; + done = true; + continue; + } + } + break; + } +} + +void nsFileControlFrame::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + NS_ENSURE_TRUE_VOID(mContent); + + // Remove the events. + if (mContent) { + mContent->RemoveSystemEventListener(u"drop"_ns, mMouseListener, false); + mContent->RemoveSystemEventListener(u"dragover"_ns, mMouseListener, false); + } + + aPostDestroyData.AddAnonymousContent(mTextContent.forget()); + aPostDestroyData.AddAnonymousContent(mBrowseFilesOrDirs.forget()); + + mMouseListener->ForgetFrame(); + nsBlockFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +static already_AddRefed<Element> MakeAnonButton( + Document* aDoc, const char* labelKey, HTMLInputElement* aInputElement) { + RefPtr<Element> button = aDoc->CreateHTMLElement(nsGkAtoms::button); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + button->SetIsNativeAnonymousRoot(); + button->SetPseudoElementType(PseudoStyleType::fileSelectorButton); + + // Set the file picking button text depending on the current locale. + nsAutoString buttonTxt; + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + labelKey, aDoc, buttonTxt); + + auto* nim = aDoc->NodeInfoManager(); + // 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 textContent = new (nim) nsTextNode(nim); + textContent->SetText(buttonTxt, false); + + IgnoredErrorResult error; + button->AppendChildTo(textContent, false, error); + if (error.Failed()) { + return nullptr; + } + + auto* buttonElement = HTMLButtonElement::FromNode(button); + // We allow tabbing over the input itself, not the button. + buttonElement->SetTabIndex(-1, IgnoreErrors()); + return button.forget(); +} + +nsresult nsFileControlFrame::CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) { + nsCOMPtr<Document> doc = mContent->GetComposedDoc(); + RefPtr fileContent = HTMLInputElement::FromNode(mContent); + + mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent); + if (!mBrowseFilesOrDirs) { + return NS_ERROR_OUT_OF_MEMORY; + } + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aElements.AppendElement(mBrowseFilesOrDirs); + + // Create and setup the text showing the selected files. + mTextContent = doc->CreateHTMLElement(nsGkAtoms::label); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + mTextContent->SetIsNativeAnonymousRoot(); + RefPtr<nsTextNode> text = + new (doc->NodeInfoManager()) nsTextNode(doc->NodeInfoManager()); + mTextContent->AppendChildTo(text, false, IgnoreErrors()); + + // Update the displayed text to reflect the current element's value. + nsAutoString value; + fileContent->GetDisplayFileName(value); + UpdateDisplayedValue(value, false); + + aElements.AppendElement(mTextContent); + + // We should be able to interact with the element by doing drag and drop. + mContent->AddSystemEventListener(u"drop"_ns, mMouseListener, false); + mContent->AddSystemEventListener(u"dragover"_ns, 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, SystemCallerGuarantee(), err); + if (err.Failed()) { + err.SuppressException(); + return; + } + + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(fullpath, 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->AsGlobal(), 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(Event* aEvent) { + NS_ASSERTION(mFrame, "We should have been unregistered"); + + if (aEvent->DefaultPrevented()) { + return NS_OK; + } + + DragEvent* dragEvent = aEvent->AsDragEvent(); + if (!dragEvent) { + return NS_OK; + } + + RefPtr<DataTransfer> dataTransfer = dragEvent->GetDataTransfer(); + if (!IsValidDropData(dataTransfer)) { + return NS_OK; + } + + RefPtr<HTMLInputElement> inputElement = + HTMLInputElement::FromNode(mFrame->GetContent()); + bool supportsMultiple = + inputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple); + if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) { + dataTransfer->SetDropEffect(u"none"_ns); + 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(); + + RefPtr<FileList> fileList = + dataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal()); + + RefPtr<BlobImpl> webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsTArray<OwningFileOrDirectory> array; + if (webkitDir) { + AppendBlobImplAsDirectory(array, webkitDir, inputElement); + inputElement->MozSetDndFilesAndDirectories(array); + } else { + bool blinkFileSystemEnabled = + StaticPrefs::dom_webkitBlink_filesystem_enabled(); + 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(), inputElement); + } else { + OwningFileOrDirectory* element = array.AppendElement(); + element->SetAsFile() = file; + } + } + } + } + } + + // Entries API. + if (blinkFileSystemEnabled) { + // 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); + inputElement->UpdateEntries(array); + } + // Normal DnD + else { + inputElement->SetFiles(fileList, true); + } + + RefPtr<TextEditor> textEditor; + DebugOnly<nsresult> rvIgnored = + nsContentUtils::DispatchInputEvent(inputElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + nsContentUtils::DispatchTrustedEvent( + inputElement->OwnerDoc(), static_cast<nsINode*>(inputElement), + u"change"_ns, CanBubble::eYes, Cancelable::eNo); + } + } + + return NS_OK; +} + +nsresult nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory( + FileList* aFileList, BlobImpl** aBlobImpl) { + *aBlobImpl = nullptr; + + HTMLInputElement* inputElement = + HTMLInputElement::FromNode(mFrame->GetContent()); + bool webkitDirPicker = + StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + inputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory); + if (!webkitDirPicker) { + return NS_OK; + } + + if (!aFileList) { + return NS_ERROR_FAILURE; + } + + // webkitdirectory doesn't care about the length of the file list but + // only about the first item on it. + uint32_t len = aFileList->Length(); + if (len) { + File* file = aFileList->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( + DataTransfer* aDataTransfer) { + if (!aDataTransfer) { + return false; + } + + // We only support dropping files onto a file upload control + return aDataTransfer->HasFile(); +} + +bool nsFileControlFrame::DnDListener::CanDropTheseFiles( + DataTransfer* aDataTransfer, bool aSupportsMultiple) { + RefPtr<FileList> fileList = + aDataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal()); + + 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) { + listLength = fileList->Length(); + } + return listLength <= 1 || aSupportsMultiple; +} + +nscoord nsFileControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + + // Our min inline size is our pref inline size + result = GetPrefISize(aRenderingContext); + return result; +} + +nscoord nsFileControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + // Make sure we measure with the uncropped filename. + if (mCachedPrefISize == NS_INTRINSIC_ISIZE_UNKNOWN) { + nsAutoString filename; + HTMLInputElement::FromNode(mContent)->GetDisplayFileName(filename); + UpdateDisplayedValue(filename, false); + } + + result = nsBlockFrame::GetPrefISize(aRenderingContext); + return result; +} + +void nsFileControlFrame::SyncDisabledState() { + if (mContent->AsElement()->State().HasState(ElementState::DISABLED)) { + mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u""_ns, + true); + } else { + mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + } +} + +void nsFileControlFrame::ElementStateChanged(ElementState aStates) { + if (aStates.HasState(ElementState::DISABLED)) { + nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this)); + } +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsFileControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"FileControl"_ns, aResult); +} +#endif + +void nsFileControlFrame::UpdateDisplayedValue(const nsAString& aValue, + bool aNotify) { + auto* text = Text::FromNode(mTextContent->GetFirstChild()); + uint32_t oldLength = aNotify ? 0 : text->TextLength(); + text->SetText(aValue, aNotify); + if (!aNotify) { + // We can't notify during Reflow so we need to tell the text frame + // about the text content change we just did. + if (auto* textFrame = static_cast<nsTextFrame*>(text->GetPrimaryFrame())) { + textFrame->NotifyNativeAnonymousTextnodeChange(oldLength); + } + nsBlockFrame* label = do_QueryFrame(mTextContent->GetPrimaryFrame()); + if (label && label->LinesBegin() != label->LinesEnd()) { + label->AddStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION); + label->LinesBegin()->MarkDirty(); + } + } +} + +nsresult nsFileControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + if (nsGkAtoms::value == aName) { + UpdateDisplayedValue(aValue, true); + } + return NS_OK; +} + +void nsFileControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + BuildDisplayListForInline(aBuilder, aLists); +} + +#ifdef ACCESSIBILITY +a11y::AccType nsFileControlFrame::AccessibleType() { + return a11y::eHTMLFileInputType; +} +#endif + +//////////////////////////////////////////////////////////// +// Mouse listener implementation + +NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener, nsIDOMEventListener) |