nsFileControlFrame.cpp (13843B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "nsFileControlFrame.h" 8 9 #include "MiddleCroppingBlockFrame.h" 10 #include "gfxContext.h" 11 #include "mozilla/Preferences.h" 12 #include "mozilla/PresShell.h" 13 #include "mozilla/StaticPrefs_dom.h" 14 #include "mozilla/TextEditor.h" 15 #include "mozilla/dom/BlobImpl.h" 16 #include "mozilla/dom/DOMStringList.h" 17 #include "mozilla/dom/DataTransfer.h" 18 #include "mozilla/dom/Directory.h" 19 #include "mozilla/dom/Document.h" 20 #include "mozilla/dom/DragEvent.h" 21 #include "mozilla/dom/Element.h" 22 #include "mozilla/dom/Event.h" 23 #include "mozilla/dom/FileList.h" 24 #include "mozilla/dom/HTMLButtonElement.h" 25 #include "mozilla/dom/HTMLInputElement.h" 26 #include "mozilla/dom/NodeInfo.h" 27 #include "mozilla/dom/UnionTypes.h" 28 #include "nsCOMPtr.h" 29 #include "nsContentCreatorFunctions.h" 30 #include "nsContentUtils.h" 31 #include "nsGkAtoms.h" 32 #include "nsIFile.h" 33 #include "nsIFrame.h" 34 #include "nsLayoutUtils.h" 35 #include "nsNodeInfoManager.h" 36 #include "nsTextFrame.h" 37 #include "nsTextNode.h" 38 39 using namespace mozilla; 40 using namespace mozilla::dom; 41 42 nsIFrame* NS_NewFileControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) { 43 return new (aPresShell) 44 nsFileControlFrame(aStyle, aPresShell->GetPresContext()); 45 } 46 47 NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame) 48 49 nsFileControlFrame::nsFileControlFrame(ComputedStyle* aStyle, 50 nsPresContext* aPresContext) 51 : nsBlockFrame(aStyle, aPresContext, kClassID) {} 52 53 void nsFileControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, 54 nsIFrame* aPrevInFlow) { 55 nsBlockFrame::Init(aContent, aParent, aPrevInFlow); 56 57 mMouseListener = new DnDListener(this); 58 } 59 60 void nsFileControlFrame::Reflow(nsPresContext* aPresContext, 61 ReflowOutput& aReflowOutput, 62 const ReflowInput& aReflowInput, 63 nsReflowStatus& aStatus) { 64 nsBlockFrame::Reflow(aPresContext, aReflowOutput, aReflowInput, aStatus); 65 66 // Form control frame should be monolithic, and cannot be split, so our reflow 67 // status should be fully-complete. 68 aStatus.Reset(); 69 } 70 71 void nsFileControlFrame::Destroy(DestroyContext& aContext) { 72 NS_ENSURE_TRUE_VOID(mContent); 73 74 // Remove the events. 75 if (mContent) { 76 mContent->RemoveSystemEventListener(u"drop"_ns, mMouseListener, false); 77 mContent->RemoveSystemEventListener(u"dragover"_ns, mMouseListener, false); 78 } 79 80 aContext.AddAnonymousContent(mTextContent.forget()); 81 aContext.AddAnonymousContent(mBrowseFilesOrDirs.forget()); 82 83 mMouseListener->ForgetFrame(); 84 nsBlockFrame::Destroy(aContext); 85 } 86 87 static already_AddRefed<Element> MakeAnonButton( 88 Document* aDoc, const char* labelKey, HTMLInputElement* aInputElement) { 89 RefPtr<Element> button = aDoc->CreateHTMLElement(nsGkAtoms::button); 90 // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any 91 // attribute. 92 button->SetIsNativeAnonymousRoot(); 93 button->SetPseudoElementType(PseudoStyleType::fileSelectorButton); 94 95 // Set the file picking button text depending on the current locale. 96 nsAutoString buttonTxt; 97 nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, 98 labelKey, aDoc, buttonTxt); 99 100 auto* nim = aDoc->NodeInfoManager(); 101 // Set the browse button text. It's a bit of a pain to do because we want to 102 // make sure we are not notifying. 103 RefPtr textContent = new (nim) nsTextNode(nim); 104 textContent->SetText(buttonTxt, false); 105 106 IgnoredErrorResult error; 107 button->AppendChildTo(textContent, false, error); 108 if (error.Failed()) { 109 return nullptr; 110 } 111 112 auto* buttonElement = HTMLButtonElement::FromNode(button); 113 // We allow tabbing over the input itself, not the button. 114 buttonElement->SetTabIndex(-1, IgnoreErrors()); 115 return button.forget(); 116 } 117 118 nsresult nsFileControlFrame::CreateAnonymousContent( 119 nsTArray<ContentInfo>& aElements) { 120 nsCOMPtr<Document> doc = mContent->GetComposedDoc(); 121 RefPtr fileContent = HTMLInputElement::FromNode(mContent); 122 123 mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent); 124 if (!mBrowseFilesOrDirs) { 125 return NS_ERROR_OUT_OF_MEMORY; 126 } 127 // XXX(Bug 1631371) Check if this should use a fallible operation as it 128 // pretended earlier, or change the return type to void. 129 aElements.AppendElement(mBrowseFilesOrDirs); 130 131 // Create and setup the text showing the selected files. 132 mTextContent = doc->CreateHTMLElement(nsGkAtoms::label); 133 // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any 134 // attribute. 135 mTextContent->SetIsNativeAnonymousRoot(); 136 RefPtr<nsTextNode> text = 137 new (doc->NodeInfoManager()) nsTextNode(doc->NodeInfoManager()); 138 mTextContent->AppendChildTo(text, false, IgnoreErrors()); 139 140 aElements.AppendElement(mTextContent); 141 142 // We should be able to interact with the element by doing drag and drop. 143 mContent->AddSystemEventListener(u"drop"_ns, mMouseListener, false); 144 mContent->AddSystemEventListener(u"dragover"_ns, mMouseListener, false); 145 146 SyncDisabledState(); 147 148 return NS_OK; 149 } 150 151 void nsFileControlFrame::AppendAnonymousContentTo( 152 nsTArray<nsIContent*>& aElements, uint32_t aFilter) { 153 if (mBrowseFilesOrDirs) { 154 aElements.AppendElement(mBrowseFilesOrDirs); 155 } 156 157 if (mTextContent) { 158 aElements.AppendElement(mTextContent); 159 } 160 } 161 162 NS_QUERYFRAME_HEAD(nsFileControlFrame) 163 NS_QUERYFRAME_ENTRY(nsFileControlFrame) 164 NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) 165 NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) 166 167 static void AppendBlobImplAsDirectory(nsTArray<OwningFileOrDirectory>& aArray, 168 BlobImpl* aBlobImpl, 169 nsIContent* aContent) { 170 MOZ_ASSERT(aBlobImpl); 171 MOZ_ASSERT(aBlobImpl->IsDirectory()); 172 173 nsAutoString fullpath; 174 ErrorResult err; 175 aBlobImpl->GetMozFullPath(fullpath, SystemCallerGuarantee(), err); 176 if (err.Failed()) { 177 err.SuppressException(); 178 return; 179 } 180 181 nsCOMPtr<nsIFile> file; 182 nsresult rv = NS_NewLocalFile(fullpath, getter_AddRefs(file)); 183 if (NS_WARN_IF(NS_FAILED(rv))) { 184 return; 185 } 186 187 nsPIDOMWindowInner* inner = aContent->OwnerDoc()->GetInnerWindow(); 188 if (!inner || !inner->IsCurrentInnerWindow()) { 189 return; 190 } 191 192 RefPtr<Directory> directory = Directory::Create(inner->AsGlobal(), file); 193 MOZ_ASSERT(directory); 194 195 OwningFileOrDirectory* element = aArray.AppendElement(); 196 element->SetAsDirectory() = directory; 197 } 198 199 /** 200 * This is called when we receive a drop or a dragover. 201 */ 202 NS_IMETHODIMP 203 nsFileControlFrame::DnDListener::HandleEvent(Event* aEvent) { 204 NS_ASSERTION(mFrame, "We should have been unregistered"); 205 206 if (aEvent->DefaultPrevented()) { 207 return NS_OK; 208 } 209 210 DragEvent* dragEvent = aEvent->AsDragEvent(); 211 if (!dragEvent) { 212 return NS_OK; 213 } 214 215 RefPtr<DataTransfer> dataTransfer = dragEvent->GetDataTransfer(); 216 if (!IsValidDropData(dataTransfer)) { 217 return NS_OK; 218 } 219 220 RefPtr<HTMLInputElement> inputElement = 221 HTMLInputElement::FromNode(mFrame->GetContent()); 222 bool supportsMultiple = inputElement->HasAttr(nsGkAtoms::multiple); 223 if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) { 224 dataTransfer->SetDropEffect(u"none"_ns); 225 aEvent->StopPropagation(); 226 return NS_OK; 227 } 228 229 nsAutoString eventType; 230 aEvent->GetType(eventType); 231 if (eventType.EqualsLiteral("dragover")) { 232 // Prevent default if we can accept this drag data 233 aEvent->PreventDefault(); 234 return NS_OK; 235 } 236 237 if (eventType.EqualsLiteral("drop")) { 238 aEvent->StopPropagation(); 239 aEvent->PreventDefault(); 240 241 RefPtr<FileList> fileList = 242 dataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal()); 243 244 RefPtr<BlobImpl> webkitDir; 245 nsresult rv = 246 GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); 247 NS_ENSURE_SUCCESS(rv, NS_OK); 248 249 nsTArray<OwningFileOrDirectory> array; 250 if (webkitDir) { 251 AppendBlobImplAsDirectory(array, webkitDir, inputElement); 252 inputElement->MozSetDndFilesAndDirectories(array); 253 } else { 254 bool blinkFileSystemEnabled = 255 StaticPrefs::dom_webkitBlink_filesystem_enabled(); 256 if (blinkFileSystemEnabled) { 257 FileList* files = static_cast<FileList*>(fileList.get()); 258 if (files) { 259 for (uint32_t i = 0; i < files->Length(); ++i) { 260 File* file = files->Item(i); 261 if (file) { 262 if (file->Impl() && file->Impl()->IsDirectory()) { 263 AppendBlobImplAsDirectory(array, file->Impl(), inputElement); 264 } else { 265 OwningFileOrDirectory* element = array.AppendElement(); 266 element->SetAsFile() = file; 267 } 268 } 269 } 270 } 271 } 272 273 // Entries API. 274 if (blinkFileSystemEnabled) { 275 // This is rather ugly. Pass the directories as Files using SetFiles, 276 // but then if blink filesystem API is enabled, it wants 277 // FileOrDirectory array. 278 inputElement->SetFiles(fileList, true); 279 inputElement->UpdateEntries(array); 280 } 281 // Normal DnD 282 else { 283 inputElement->SetFiles(fileList, true); 284 } 285 286 RefPtr<TextEditor> textEditor; 287 DebugOnly<nsresult> rvIgnored = 288 nsContentUtils::DispatchInputEvent(inputElement); 289 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), 290 "Failed to dispatch input event"); 291 nsContentUtils::DispatchTrustedEvent(inputElement->OwnerDoc(), 292 inputElement, u"change"_ns, 293 CanBubble::eYes, Cancelable::eNo); 294 } 295 } 296 297 return NS_OK; 298 } 299 300 nsresult nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory( 301 FileList* aFileList, BlobImpl** aBlobImpl) { 302 *aBlobImpl = nullptr; 303 304 HTMLInputElement* inputElement = 305 HTMLInputElement::FromNode(mFrame->GetContent()); 306 bool webkitDirPicker = StaticPrefs::dom_webkitBlink_dirPicker_enabled() && 307 inputElement->HasAttr(nsGkAtoms::webkitdirectory); 308 if (!webkitDirPicker) { 309 return NS_OK; 310 } 311 312 if (!aFileList) { 313 return NS_ERROR_FAILURE; 314 } 315 316 // webkitdirectory doesn't care about the length of the file list but 317 // only about the first item on it. 318 uint32_t len = aFileList->Length(); 319 if (len) { 320 File* file = aFileList->Item(0); 321 if (file) { 322 BlobImpl* impl = file->Impl(); 323 if (impl && impl->IsDirectory()) { 324 RefPtr<BlobImpl> retVal = impl; 325 retVal.swap(*aBlobImpl); 326 return NS_OK; 327 } 328 } 329 } 330 331 return NS_ERROR_FAILURE; 332 } 333 334 bool nsFileControlFrame::DnDListener::IsValidDropData( 335 DataTransfer* aDataTransfer) { 336 if (!aDataTransfer) { 337 return false; 338 } 339 340 // We only support dropping files onto a file upload control 341 return aDataTransfer->HasFile(); 342 } 343 344 bool nsFileControlFrame::DnDListener::CanDropTheseFiles( 345 DataTransfer* aDataTransfer, bool aSupportsMultiple) { 346 RefPtr<FileList> fileList = 347 aDataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal()); 348 349 RefPtr<BlobImpl> webkitDir; 350 nsresult rv = 351 GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); 352 // Just check if either there isn't webkitdirectory attribute, or 353 // fileList has a directory which can be dropped to the element. 354 // No need to use webkitDir for anything here. 355 NS_ENSURE_SUCCESS(rv, false); 356 357 uint32_t listLength = 0; 358 if (fileList) { 359 listLength = fileList->Length(); 360 } 361 return listLength <= 1 || aSupportsMultiple; 362 } 363 364 void nsFileControlFrame::SyncDisabledState() { 365 if (mContent->AsElement()->State().HasState(ElementState::DISABLED)) { 366 mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u""_ns, 367 true); 368 } else { 369 mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); 370 } 371 } 372 373 void nsFileControlFrame::ElementStateChanged(ElementState aStates) { 374 if (aStates.HasState(ElementState::DISABLED)) { 375 nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this)); 376 } 377 } 378 379 #ifdef DEBUG_FRAME_DUMP 380 nsresult nsFileControlFrame::GetFrameName(nsAString& aResult) const { 381 return MakeFrameName(u"FileControl"_ns, aResult); 382 } 383 #endif 384 385 void nsFileControlFrame::SelectedFilesUpdated() { 386 if (MiddleCroppingBlockFrame* f = 387 do_QueryFrame(mTextContent->GetPrimaryFrame())) { 388 f->UpdateDisplayedValueToUncroppedValue(true); 389 } 390 } 391 392 #ifdef ACCESSIBILITY 393 a11y::AccType nsFileControlFrame::AccessibleType() { 394 return a11y::eHTMLFileInputType; 395 } 396 #endif 397 398 NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener, nsIDOMEventListener) 399 400 class FileControlLabelFrame final : public MiddleCroppingBlockFrame { 401 public: 402 NS_DECL_QUERYFRAME 403 NS_DECL_FRAMEARENA_HELPERS(FileControlLabelFrame) 404 405 FileControlLabelFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) 406 : MiddleCroppingBlockFrame(aStyle, aPresContext, kClassID) {} 407 408 HTMLInputElement& FileInput() const { 409 return *HTMLInputElement::FromNode(mContent->GetParent()); 410 } 411 412 void GetUncroppedValue(nsAString& aValue) override { 413 return FileInput().GetDisplayFileName(aValue); 414 } 415 }; 416 417 NS_QUERYFRAME_HEAD(FileControlLabelFrame) 418 NS_QUERYFRAME_ENTRY(FileControlLabelFrame) 419 NS_QUERYFRAME_TAIL_INHERITING(MiddleCroppingBlockFrame) 420 NS_IMPL_FRAMEARENA_HELPERS(FileControlLabelFrame) 421 422 nsIFrame* NS_NewFileControlLabelFrame(PresShell* aPresShell, 423 ComputedStyle* aStyle) { 424 return new (aPresShell) 425 FileControlLabelFrame(aStyle, aPresShell->GetPresContext()); 426 }